被awd速通(1)


心血来潮学习awd(吗?)

awd常见于线下赛,刚开始每个队都会有相同的初始环境。这些环境运行着一些特定的服务和应用,需要利用这些漏洞,获取flag以获得积分,同时也需要修补自身漏洞进行防御,以防被交flag导致丢分

流程

  • 准备:多个靶机服务器,有sshvnc的用户名或者密码,以及相关的ip信息
  • 加固:自行登录服务器并进行安全加固。需要进行以下流程
    • 备份源码
    • 改密码(包括但不限于ssh密码、mysql密码、windows等密码)
    • 代审(d盾?),找明显的洞
    • 针对洞进行修复
  • 攻击,通过对别的靶机服务进行攻击获取得分

比赛靶机情况

  • 运维机Windows 10 + 攻击机kali linux + win靶机(Winserver 2003/2008/2012)或者win7 + linux靶机Centos7.x / Ubuntu 16.04/17.01/20.04
  • 运维机Windows10 + 攻击机kali linux + linux靶机
  • 运维机 + 攻击机 + 纯windows靶机

也就是说比赛会有数台机子,可能windows,也有可能是linux,这些机子上面的web服务等地方都有漏洞,要先抓紧时间修复,再通过找到的漏洞去攻击别的队伍的服务器拿到flag从而得分

修复流程

备份源码

  • web源码备份:

windows机的话,建议直接打包

web服务路径:

1
2
3
4
5
6
7
8
php:
/var/www/html

java python等比较陌生的目录可以使用以下的方法来获取:
ps -aux | grep python #获取进程用户
ps -aux | grep ctf #获取用户启动的进程

#一般能够找到java的web启动脚本目录

打包:

1
2
tar -cvf web.tar /var/www/html
zip -q -r web.zip /var/www/html

解压缩

1
2
tar -xvf web.tar -c /var/www/html
unzip web.zip -d /var/www/html

备份:

1
2
mv web.tar /tmp #也可以是其他的路径
mv web.zip /tmp #也可以是其他的路径

上传和下载源码:

1
2
3
4
scp username@servername:/path/filename /tmp/local_destination  #从服务器下载单个文件到本地
scp /path/local_filename username@servername:/path #从本地上传单个文件到服务器
scp -r username@servername:remote_dir/ /tmp/local_dir #从服务器下载整个目录到本地
scp -r /tmp/local_dir username@servername:remote_dir #从本地上传整个目录到服务器
  • mysql数据库备份
  • 检查mysqldump是否在/usr/sbin等其他目录下
  • 如果靶机没有需要提前准备编译好的工具:
1
mysqldump -u username -p password databasename >target.sql

备份所有数据库

1
mysqldump -all -databases > all.sql

导入数据库

1
mysql -u username -p password database < from.sql

source导入mysql方法:

前提:需要创建好数据库,且该数据库必须与你的备份文件中的库名一样

1
2
use db_name;
source /tmp/xxxx.sql
  • 备份运行进程命令:
1
ps aux > /tmp/ps.txt

改密码

ssh:

1
2
passwd admin
passwd root

一些后台密码可能默认会存在于一些类似于admin.phplogin.php当中,还有可能存在于db.sql等数据库当中,记得修改密码

1
update 表名 set 字段1=值1,字段2=值2..;

改mysql密码:

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
mysql -u root -p #登录
set password for username@localhost = password(新密码)

#方法2
mysqldamin -u用户名 -p旧密码 password 新密码

#方法3
#登录root
use mysql;
update mysql.user set authentication_string=password('新密码') where user='用户名' and Host = 'localhost';
flush privileges;
quit;
#该方法不适用于mysql8

#方法4 mysql8
#需要先检查authentication_string是否为空
use mysql;
update user set authentication_string = '' where user = 'root';
#先置空该字段↑

#修改密码为root↓
alter user 'root'@'localhost' identified by 'root'

#若报错,则需刷新一下权限
flush privileges;
#再执行修改密码的操作
alter user 'root'@'localhost' identified by 'root'

redis:

1
2
3
4
5
nc 127.0.0.1 6379

auth passwd

config set requirepass mima

windows加固

查看信息

1
2
whoami /all
ipconfig /all

加固445端口、防火墙3389服务:

  1. 开启系统日志审计功能
  2. 禁用guest账户、关闭文件共享
  3. 确保启动项内容是可控的
  4. 限制3389远程访问控制的连接数:在本地组策略编辑器里面,依次展开计算机配置–>管理模板–>Windows组件–>远程桌面服务–>远程桌面会话主机–>连接–>限制连接的数量
  5. 使用工具监控关键目录文件:文件操作监控.exe、御剑文件监控.exe
  6. 恶意代码文件,通过PCHunter、Monitor查找
  7. Web目录环境查找相关可疑文件:jpg/png/rar,查看属性、解压看文件内容
  8. NTFS扫描磁盘查找隐藏的交换流数据
  9. 查找系统所有账户信息,禁止非Administrator账户

查看端口:

1
2
3
4
5
netstat #查看活动链接
netstat -ano/-a #查看端口情况
netstat -anp #查看端口
firewall-cmd --zone=public --remove-port=80/tcp -permanent #关端口
firewall-cmd -reload #重启防火墙

RDP:

删除默认帐户并手动添加新用户:

  • 步骤1:按 Win + R 打开运行对话框,输入 secpol.msc 并单击 “确定”
  • 步骤2:导航至此处:本地策略–>用户权限分配,再双击打开 “允许通过远程桌面服务登录”
  • 步骤3:删除此窗口中列出的管理员和远程桌面用户(或计算机上的任何其他用户或组)
  • 步骤4:之后单击 “添加用户或组” 并手动添加您要授予远程桌面访问权限的用户

更改默认RDP端口号:

  • 步骤1:打开运行对话框,输入 regedit 并单击 “确定”
  • 步骤2:打开 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp ,向下滚动并找到 PortNumber 然后双击它
  • 步骤3:选择 “十进制”,修改为您想要设置的端口号,然后单击 “确定”

linux加固

1
2
3
4
5
6
改密码
.bash_history查找历史命令
crontab -l 查看计划任务
crontab -e 编辑
cat /etc/init.d/rc.local 查看启动服务有无异常
iptable
1
2
3
4
5
6
7
8
9
uname -a #内核
ps -aux #进程
ps -ef | grep 进程名称 #筛选进程
id
cat /etc/passwd
ls /home/
find / -type d -perm -002 #可写目录检查
ifconfig
ip addr show #查网卡信息

查看端口:

1
2
3
4
5
netstat #查看活动链接
netstat -ano/-a #查看端口情况
netstat -anp #查看端口
firewall-cmd --zone=public --remove-port=80/tcp -permanent #关端口
firewall-cmd -reload #重启防火墙

SSH安全加固:

限制ip登录

1
2
sudo nano /etc/ssh/sshd_config       //以root权限编辑SSH配置文件
AllowUsers username@192.168.0.100 //找到并编辑以下行,确保其取消注释并设置为所需的IP地址

禁用root远程登录(同上配置):

1
2
sudo nano /etc/ssh/sshd_config       //以root权限编辑SSH配置文件
PermitRootLogin no //将PermitRootLogi设置为“no”

按用户和组限制SSH登录

1
2
3
4
5
sudo nano /etc/ssh/sshd_config       //以root权限编辑SSH配置文件
AllowUsers testuser //设置只允许 testuser 登录SSH
AllowUsers testuser@192.168.1.100 //设置只允许 192.168.1.100 的机器用 testuser 账户登录SSH
AllowGroups test //设置用户组白名单
//需要注意的是:如果同时指定了 AllowUsers 与 AllowGroups 那么必须要在两个选项中都匹配到的用户才能进行SSH登录

重启SSH服务

1
2
sudo service sshd restart
sudo systemctl restart sshd.service

信息收集

寻找配置文件

1
find  /var/www/html -path '*config*'

如果是java,将jar包解压之后看一下BOOT-INF/lib下的依赖,是否有shiro等依赖,若有,再检查ShiroConfig.class或者类似的地方是否有存在shiro的默认密钥

寻找flag

1
2
3
4
5
#linux下,在web目录中找flag
grep -r "flag" /var/www/html

#windows下,从当前目录和所有的子目录下的所有文件查找flag
findstr /s /i "flag" *.*

禁ping:

1
2
3
echo "1" > /proc/sys/net/ipv4/icmp_echo_ignore_all #临时开启禁ping

echo "0" > /proc/sys/net/ipv4/icmp_echo_ignore_all #关闭禁ping

应急响应

1
2
3
netstat
ps -aux
netstat -apt

杀进程:

1
2
3
kill -9 pid

taskkill /f /pid pid

搜webshell:

1
2
3
4
find /var/www/html -name *.php -mmin -5                        //查看最近5分钟修改文件
find ./ -name '*.php' | xargs wc -l | sort -u //寻找行数最短文件,一般有可能是一句话木马
grep -r --include=*.php '[^a-z]eval($_POST' /var/www/html //查包含关键字的php文件
find /var/www/html -type f -name "*.php" | xargs grep "eval(" |more //在Linux系统中使用find、grep和xargs命令的组合,用于在指定目录(/var/www/html)下查找所有以.php为扩展名的文件,并搜索这些文件中包含字符串"eval("的行,并使用more命令来分页显示结果以便在输出较长时进行逐页查看

不死马:

高权限下,可以直接重启服务:

1
2
3
4
5
#ubuntu
service apache2 start

#centos
systemctl apache2 start

但是基本上比赛的时候不会放出高权限的账户,得在低权限下进行处理:

del.php

1
2
3
4
5
6
7
<?php
//www-data,用户名不一定是www-data,要看情况
system("kill `ps -aux | grep www-data | grep -v grep | awk '{print $2}' | xargs kill -9`");
//杀php-fpm
system("kill `ps -ef | grep php-fpm | grep -v grep | awk '{print $2}'`");
//杀httpd php-apache
system("kill `ps -ef | grep httpd | grep -v grep | awk '{print $2}'`");

访问后再删除不死马

也可以先利用ps命令查看进程的pid

或者我们也可以用ps -aux命令来查看每个用户执行的命令和运行的进程

然后:

1
2
3
4
5
6
7
<?php
while (1) {
$pid=1234; //进程的pid
@unlink('.shell.php');
exec('kill -9 $pid');
}
?>

方法2:创建一个和不死马同名的文件夹

1
2
3
#!/bin/bash
cd /var/www/html/
while true;do rm -rf .l.php;mkdir .l.php;done

运行时:

1
nohup bash 1.sh &

方法3:竞争写入无意义的一句话:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '.l.php';
$code = '<?php echo "awa"; ?>';
while (1){
file_put_contents($file,$code);
//system('touch -m -d "2018-12-01 09:10:12" .l.php');
//usleep(1000);
usleep(0);
}
?>
1
ps -aux | grep www-data | grep -v grep | awk '{print $2}' | xargs kill -9

查杀弹shell

1
2
3
ps -ef
px -aux
ps -aux | grep www-data

检查是否有/bin/sh或者/bin/bash

很有可能是nc

1
kill `ps -aux | grep www-data | grep apache2 | awk '{print $2}'`

变种不死马:

文件名:

1
-index.php等
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '-index.php';
$code = '<?php if(md5($_GET["pass"])=="56183c1f36ef08fb8b027a4116db8483"){@eval($_POST["a"]);} ?>';
//pass=lewiserii
while (1){
file_put_contents($file,$code);
//system('touch -m -d "2018-12-01 09:10:12" -index.php');
usleep(0);
}
?>

删除只需要添加

1
2
3
./
例如
rm -rf ./-index.php

web服务加固

php

sql注入

  • 预编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$mysqli = new mysqli('localhost', 'root', 'root', 'db');
$mysqli->query('set names utf8');

#创建预编译对象
$mysqli_stmt = $mysqli->prepare('select name balance from account where id = ?');

//用户输入的参数
$id = $_GET['id'];

//绑定参数
$mysqli_stmt->bind_param("i", $id);

//执行
$mysql_stmt->execute();

//绑定结果集
$mysqli_stmt->bind_result($name, $balance);

//输出结果集
while($mysqli_stamt->fetch()){
echo "$name--$balance";
}

最佳的解决方式,预编译,但是不能防order by(业务需要,导致这个参数如果被预编译了就无法执行语句了)

对于预编译就上白名单即可

  • addslashesmysqli_real_escape_string

其中mysqli_real_escape_string还能够防止宽字节注入

  • php 5.4以下,禁用魔术引号(自动对外部来源数据进行转义,防止sql注入)
1
magic_quotes_gpc=off
  • waf启动:
1
$filter = "regexp|from|count|procedure|and|ascii|substr|substring|left|right|union|if|case|pow|exp|order|sleep|benchmark|into|load|outfile|dumpfile|load_file|join|show|select|update|set|concat|delete|alter|insert|create|union|or|drop|not|for|join|is|between|group_concat|like|where|user|ascii|greatest|mid|substr|left|right|char|hex|ord|case|limit|conv|table|mysql_history|flag|count|rpad|\&|\*|\.|-"; if((preg_match("/".$filter."/is",$username)== 1) || (preg_match("/".$filter."/is",$password)== 1)){ die(); }
  • 记得关闭报错信息
1
error_reporting(0);

xss

  • httponly

  • htmlspecialchars()转义函数:

1
2
3
4
<?php if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) { 
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); $name = htmlspecialchars( $_GET[ 'name' ] ); echo "<pre>Hello ${name}</pre>";
}
?>

htmlspecialchars()能够将预定义的字符转换为html实体

1
2
3
4
htmlspecialchars(string,flags,character-set,double_encode) 其中第二个参数flags需要重要注意,很多开发者就是因为没有注意到这个参数导致使用htmlspecialchars()函数过滤XSS时被绕过。因为flags参数对于引号的编码如下: 可用的引号类型: 
ENT_COMPAT - 默认。仅编码双引号。
ENT_QUOTES - 编码双引号和单引号。
ENT_NOQUOTES - 不编码任何引号。 默认是只编码双引号的

前端js修复:

1
2
3
4
5
6
7
8
String parameter = request.getParameter("xss");
parameter = ESAPI.encoder().encodeForHTML(parameter);
//owasp官方api进行html实体编码
response.getWriter().write("<body>"+parameter+"</body>");
waf
function wafxss($str){
return !preg_match("/\'|http|\"|\`|cookie|<|>|script/i", $str);
}

xxe

1
 libxml_disable_entity_loader(true);

文件上传

  • 后端代码限制上传的文件类型和后缀大小:
1
if (($_FILES["Up10defile"]["type"]=="image/gif")&&(substr($_FILES["Up10defile"]["name"], strrpos($_FILES["Up10defile"]["name"], '.')+1))=='gif')&&($_FILES["file"]["size"]<1024000){ } else{ die(); }
  • 检查文件后缀:

php、php5、php4、php3、php2、phtml、pht、.htaccess、.user.ini

1
2
3
4
5
6
7
8
9
10
11
$black = array("php", "php5", "php4", "php3", "php2","phtml", "pht", "htaccess", "ini", "shtml","php7");

$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if(in_array($extension, $black) && ($_FILES["file"]["size"]>1024000) ){
$fake = md5(time());
die("上传成功, 文件路径/uploads/$fake.$extension");
}
else{
...
}
  • 检查文件内容:
1
2
3
4
5
6
7
//接上文
else{
$content = file_get_contents($_FILES["file"]["tmp_name"][0]){
if(preg_match("/system|define|flag|exec|base64|eval|php|AddType|Files/i",$post)) { echo "上传成功,审核通过后将会展示您的图片!"; exit();
}
}
}
  • 配置.htaccess
1
2
3
4
5
6
7
8
9
10
11
12
<Directory "/var/www/html/upload">   //指定目录后续的指令将应用于该目录
Options -ExecCGI -Indexes //禁用了目录中的 CGI 执行和目录索引(显示目录内容列表)功能。
AllowOverride None //不允许在该目录中使用 .htaccess 文件来覆盖服务器的配置。
RemoveHandler .php .phtml .php3 .pht .php4 .php5 .php7 .shtml
RemoveType .php .phtml .php3 .pht .php4 .php5 .php7 .shtml
//这两个指令移除指定文件扩展名的处理器和类型。
//在这种情况下,这些指令从 Apache 的处理列表中移除了与 PHP 相关的扩展名和服务器端包含(SSI)文件类型。
php_flag engine off //这个指令将 PHP 的引擎标志(engine)设置为关闭状态,从而禁用了在该目录中执行 PHP 脚本的能力。
<FilesMatch ".+\.ph(p[3457]?|t|tml)$">
deny from all
</FilesMatch> //这三行命令使用正则表达式匹配了以 .php、.phtml、.php3、.pht、.php4、.php5、.php7、.shtml 结尾的文件,并将其访问权限设置为拒绝所有
</Directory>
  • 强制给上传的文件添加后缀名,在不存在文件包含漏洞的情况下,该方法能最有效的防御攻击者上传执行木马
1
2
3
4
5
6
7
8
9
10
11
12
if (file_exists("upload_file/" . $_FILES["Up10defile"]["name"]))
{
echo $_FILES["Up10defile"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["Up10defile"]["tmp_name"],
"upload_file/" .md5(md5($_FILES["Up10defile"]["name"]).time()).".gif");
#echo "Stored in: " . "upload_file/" . $_FILES["Up10defile"]["name"].".gif";
//玩点恶心的
echo "Stored in: " . "upload_file/" . $_FILES["Up10defile"]["name"].".gif";
}

ssrf

ssrf的修复方法如下:

  • 去除url中的特殊字符
  • 判断是否属于内网ip
  • 将请求时的域名改为ip
  • 请求的url就变成了上面返回的url
  • 请求的时候设置header里的host为ip
  • 请求头,判断是否为30x的跳转

第一步为了防止url parse造成的解析问题

第三步防止dns重定向

第五步防止ip请求的时候某些网站无法访问

最后一步防止通过跳转进行绕过

非必要不要从用户那里接受要访问的url,防止用户构造url进行穿透访问

修复:

  • 利用open_basedir

open_basedir限制用户的访问目录,可以有效防止file_get_contentscurlfopen等函数对服务器的敏感文件的访问

上白名单:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php
declare(strict_types = 1);
$$url = $$_GET['url'];
// 请求url信息
$$urlInfo = parse_url($$url);
// 限制请求协议,仅允许http和https请求,防止类似file:///、gopher://、ftp://等引起的安全问题
$validSchemeList = ['http', 'https'];
// 限制允许请求到的地址
$validHostList = ['www.baidu.com'];
// 限制请求的端口为http常用的端口
$validPortList = ['80', '443'];
// 只允许用户访问特定的文件类型
$validTypeList = ['html', 'json', 'gif', 'png', 'jpeg', 'jpg'];
// 不允许访问本地指定的host(避免被用来获取内网数据)
$invalidHostList = ['127.', 'localhost', '192.', '10.'];
if (!in_array(
$urlInfo['scheme'], $
validSchemeList, true)) {
die('访问协议非法');
}
if (!in_array(
$urlInfo['port'], $
validPortList, true)) {
die('访问端口非法');
}
if (!in_array(
$urlInfo['host'], $
validHostList, true)) {
die('请求地址非法');
}
$$visitType = pathinfo($$url, PATHINFO_EXTENSION);
if (!in_array(
$visitType, $
validTypeList, true)) {
die('访问类型非法');
}
foreach (
$invalidHostList as $
invalidHost) {
if (strpos(
$urlInfo['host'], $
invalidHost) == 0) {
die('访问地址非法');
}
}
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function safe_request($url){
$ch = CURL_INIT();
CURL_SETOPT($ch, CURLOPT_HEADER, FALSE);
CURL_SETOPT($ch, CURLOPT_RETURNTRANSFER, TRUE);
CURL_SETOPT($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
while(true){
// 0.判断URL合法性
if (!$url || !filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED & FILTER_FLAG_HOST_REQUIRED & FILTER_FLAG_QUERY_REQUIRED)){
return false;
}

// 1.仅允许http或https协议
if(!preg_match('/^https?:\/\/.*$/', $url)){
return false;
}

// 2.解析目标URL,获取其host
$host = parse_url($url, PHP_URL_HOST);
if(!$host){
return false;
}

// 3.解析host,获取host指向的IP地址
$ip = gethostbyname($host);
$ip = ip2long($ip);
if($ip === false){
return false;
}

// 4.检查IP地址是否为内网IP
$is_inner_ipaddress = ip2long('127.0.0.0') >> 24 == $ip >> 24 or
ip2long('10.0.0.0') >> 24 == $ip >> 24 or
ip2long('172.16.0.0') >> 20 == $ip >> 20 or
ip2long('192.168.0.0') >> 16 == $ip >> 16;
if($is_inner_ipaddress){
return false;
}

// 5.请求URL
CURL_SETOPT($ch, CURLOPT_URL, $url);
$res = CURL_EXEC($ch);
$code = curl_getinfo($ch,CURLINFO_HTTP_CODE);

// 6.如果有跳转,获取跳转URL执行1, 否则返回响应
if (300<=$code and $code<400){
$headers = curl_getinfo($ch);
$url=$headers["redirect_url"];
} else {
CURL_CLOSE($ch) ;
return $res;
}
}
}

$url = $_GET['url'];
// $url="http://localhost:8888/302.php";
$res=safe_request($url);
if($res)
echo var_dump($res);
?>
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
/*
* 判断是否合法的URL,合法则返回true
* 不合法的情况:
* 1. 包含非ASCII码字符
* 2. 不是http或https协议
* 3. ipv6地址(不支持ipv6地址,将判断为不合法)
* 4. URL的user、pass、host,fragment段包含@、\或其它字符
* 5. 请求地址为内网IP:192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,127.0.0.1/8
* 6. 请求地址为169.254.169.254,100.100.100.200,192.0.0.192这几个常见云主机metadata地址ip
*
*
* 注意,该方法未考虑到:
* 1. 30X跳转的location响应头部是否为合法URL
* 2. DNS Rebinding攻击
*
* @author Ovie
* @param string $url
* @return bool
*/
function is_allowed_URL($url)
{
// Only allow ASCII URL (so will forbid CRLF and other Unicode char)
if (!$url || !filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED & FILTER_FLAG_HOST_REQUIRED & FILTER_FLAG_QUERY_REQUIRED)) {
return false;
}

// Only allow http and https scheme
if (!preg_match('/^https?:\/\/.*$/', $url)) {
return false;
}

$parts = parse_url($url);
foreach (array("user", "pass", "host", "fragment") as $key) {
if (isset($parts[$key]) && preg_match("~[:/?#\\\\@]~", $parts[$key])) {
return false;
}
}

// gethostbyname do not support ipv6
$ip = gethostbyname($parts['host']);
$ip = ip2long($ip);
if ($ip === false) {
return false;
}

$is_inner_ipaddress = ip2long('127.0.0.0') >> 24 == $ip >> 24 ||
ip2long('10.0.0.0') >> 24 == $ip >> 24 ||
ip2long('172.16.0.0') >> 20 == $ip >> 20 ||
ip2long('192.168.0.0') >> 16 == $ip >> 16 ||
$ip == 0 ||
ip2long('169.254.169.254') == $ip ||
ip2long('192.0.0.192') == $ip ||
ip2long('100.100.100.200') == $ip;
if ($is_inner_ipaddress) {
return false;
}

return true;
}
/*
if(isset($_GET['url'])){
$url = $_GET['url'];
var_dump(is_allowed_URL($url));
}
*/
function test($url)
{
$res = is_allowed_URL($url);
echo $url . " : " . ($res ? $res : "false") . "\n";
}

test("https://127.0.0.1:8080/ppppp");
test("https://169.254.169.254:8080/ppppp");
test("https://192.0.0.192:8080/ppppp");
test("https://100.100.100.200:8080/ppppp");
test("https://127.0.0.1:8080\@baidu.com/ppppp");
test("https://baidu.com#@127.0.0.1:8080/ppppp");
test("https://127.1:8080/ppppp");
test("https://127.0.0.1.:8080/ppppp");
test("https://2130706433:8080/ppppp");
test("https://0:8080/ppppp");
test("https://[::]:8080/ppppp");
test("https://localhost:8080/ppppp");
test("https://0:0:0:0:0:ffff:127.0.0.1:8080/ppppp");
test("https://127.0000.000000.000001:8080/ppppp");
test("https://127.0.0.1:8080#@baidu.com/ppppp");
test("http://[::1]/");
test("https://0.0.0.0:8080/ppppp");
test("https://127.2.0.2:8080/ppppp");
test("https://0x7f.0x0.0x0.0x1:8080/ppppp");
test("https://baidu.com.127.0.0.1.nip.io:8080/ppppp");
test("https://baidu.com@127.0.0.1:8080@baidu.com/ppppp");
test("https://127.0.0.1:8080\.baidu.com/ppppp");
test("https://127.0.0.1:8080:443/ppppp");
?>

python:

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
31
import socket
import re
from urllib.parse import urlparse
from socket import inet_aton
from struct import unpack

def check_ssrf(url):
hostname = urlparse(url).hostname

def ip2long(ip_addr):
return unpack("!L", inet_aton(ip_addr))[0]

def is_inner_ipaddress(ip):
ip = ip2long(ip)
return ip2long('127.0.0.0') >> 24 == ip >> 24 or \
ip2long('10.0.0.0') >> 24 == ip >> 24 or \
ip2long('172.16.0.0') >> 20 == ip >> 20 or \
ip2long('192.168.0.0') >> 16 == ip >> 16 \
ip2long('0.0.0.0') >> 24 == ip >> 24

try:
if not re.match(r"^https?://.*/.*$", url):
raise BaseException("url format error")
ip_address = socket.getaddrinfo(hostname, 'http')[0][4][0]
if is_inner_ipaddress(ip_address):
raise BaseException("inner ip address attack")
return True, "success"
except BaseException as e:
return False, str(e)
except:
return False, "unknow error"

rce

一些命令执行函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 代码执行函数 eval()、assert()、call_user_func()、call_user_func_array()、array_map()、exec()、shell_exec()等 

#不常见的:
#create_function()、array_walk()、array_map()、array_filter()、array_reduce()、array_diff_ukey/array_diff_uassoc()、usort()、ob_start()、header_register_callback('phpinfo');

#prtnl_exec()、popen()、proc_open()...

#escapeshellarg() + escapeshellcmd()连用导致的问题

# 正则处理 mixed preg_replace ( mixed $ pattern , mixed $ replacement , mixed $ subject [, int $ limit = -1 [, int &$ count ]] )
preg_replace() 参数/e修饰符问题

# 调用函数过滤不严 call_user_func()和array_map()

修复方式:

  • waf:
1
2
3
4
5
6
7
<?php $a=$_GET['cc']; 
$pattern = "eval|assert|passthru|pcntl_exec|exec|system|escapeshellcmd|popen|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|ob_start";
if(preg_match("/".$pattern."/is",$cc)== 1){
die();
}
$bb="phpinfo()";
call_user_func($cc,$bb); ?>
1
2
3
4
5
function wafrce($str){
return !preg_match("/openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|scandir|assert|pcntl_exec|fwrite|curl|system|eval|assert|flag|passthru|exec|chroot|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore|phpinfo|file_put_contents\(\.\*\$|base64_decode\(/i", $str);
}
const blacklists = [", \\, |, &, +, -, *, /, ^];
const blackwords = [`select`, `drop`, `insert`, `update`, `delete`, `like`, `order`, `truncate`, `create`, `reg`, `sub`, `left`, `right`, `mid`, `if`, `log`, `pro`, `func`, `history`, `file`, `plugin`, `role`, `collation`, `event`];
  • 修补preg_replace的时候需要将修饰符/e去掉

  • php7下,preg_replace不再适用\e,而应当使用preg_replace_callback()

  • php.ini中设置disable_function

1
disable_functions=call_user_func,call_user_func_array,array_map,array_filter,ob_start,phpinfo,eval,assert,passthru,pcntl_exec,exec,system,escapeshellcmd,popen,chroot,scandir,chgrp,chown,shell_exec

反序列化

  • php7,为unserialize()函数提供了过滤,这个特性可以提供更加安全的过滤方式
1
2
3
4
5
6
7
8
9
<?php
//将所有的对象都转换为__PHP_Incomplete_Class对象
$data = unserialize($foo, ["allowed_classes" => false]);

//将除MyClass和MyClass2之外的所有对象都转化为__PHP_Incomplete_Class对象
$data = unserialize($foo, ["allowed_classes" => ["MyClass", "MyClass2"]]);

//默认情况下,无第二个参数,或者设置为了true
$data = unserialize($foo, ["allowed_classes" => true]);
  • 限制session反序列化

php_serialize 在5.5版本后新加的一种规则,5.4及之前版本,如果设置成php_serialize会报错。

1
2
3
ini_set('session.serialize_handler', 'php_serialize');
ini_set('session.serialize_handler', 'php');
#二者处理session的方式不同,错误的使用会形成基于session的反序列化漏洞
  • 限制phar:
1
2
3
4
5
$filter = "phar|zip|compress.bzip2|compress.zlib";

if(preg_match("/".$filter."/is",$name)== 1){
die();
}
  • php.ini设置禁用函数:
1
disable_functions=fileatime,filectime,file_exists,file_get_contents,file_put_content,filegroup,fileinode,filemtime,fileowner,fileperms,is_dir,is_executable,is_file,is_link,is_readable,is_writable,is_writeable,fopen,readfile,unlink,parse_ini_file,file,copy,stat,serialize,unserialize,__construct,__destruct,__toString,__sleep,__wakeup,__get,__set,__isset,__unset,__invoke

文件包含

小waf:

1
2
3
4
5
6
7
8
9
10
<?php
$filename = $_GET['filename'];
$pattern = "\/|\.\.\/|\.\/|etc|var|php|jpg|jpeg|png|bmp|gif";

if(preg_match("/".$pattern."/is",$filename)==1){
echo "die";
die();
}
include($filename);
?>
  • 大waf(协议过滤+路径 访问控制):
1
2
3
4
5
6
7
8
9
10
$filename  = $_GET['filename'];

$pattern = "\/|\.\.\/|\.\/|etc|var|php|jpg|jpeg|png|bmp|gif|file|http|ftp|php|zlib|data|glob|phar|ssh2|rar|ogg|expect|zip|compress|filter|input";

if(preg_match("/".$pattern."/is",$filename)== 1){
echo "die00000000000000000000000000000";
die();
}

include($filename);
  • php.ini
1
2
allow_url_fopen = off  (是否允许打开远程文件)  
allow_url_include = off(是否允许include/require远程文件)
  • open_basedir
1
open_basedir="/var/www/html"

java

sql注入

  • MyBatis框架:
1
2
3
4
<if test="outtype !=null and outtype !=''">
AND u.catelog_name = '${outtype}'
</if>
<if test="baseKey !=null and baseKey !=''">

$改为#

1
2
3
4
<if test="outtype !=null and outtype !=''">
AND u.catelog_name = #{outtype}
</if>
<if test="baseKey !=null and baseKey !=''">

但是遇到了像是这种除了带有单引号,还有特殊字符,如%的要特殊处理

1
2
3
4
5
<select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User">
<!-- 拼接 MySQL,引起 SQL 注入 -->
SELECT * FROM user WHERE username LIKE '%${value}%'
</select>

修复方式要加concat:

1
2
3
4
5
<select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User">
<!-- 使用 SQL concat 语句,拼接字符串,防止 SQL 注入 -->
SELECT * FROM USER WHERE username LIKE CONCAT('%',#{value},'%' )
</select>

其实原理就像是预编译(

  • dao 和 Hibernate

DAO的漏洞代码跟mysql是类似的:

1
String sql = "select * from user where id=" + id;

也是直接进行一个拼接

Hibernate:

1
session.createQuery("from Book where title like '%' + userInput + '%' and published = true");

修复方式

预编译启动(使用java.sql.PreparedStatement):

1
2
3
4
PreparedStatement stmt = connection.prepareStat(sqlString);
stmt.setString(1, username);
stmt.setString(2, itemName);
result = stmt.executeQuery();

同理,预编译并不能防御order by导致的注入

java-ssrf

java的ssrf局限性比较大,能够发出网络请求的几个类:

  • HttpClient

apache的httpClient组件:

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
pubilc class TestAction extends ActionSupport{
public String execute() throes Exception{
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
String url = request.getParameter("url");
if(url!=null&&url.length()!=0){
CloseableHttpClient httpclient = Httpclient.execute(httpGet);
try{
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response1 = httpclient.execute(httpGet);
try{
HttpEntity entity1 = response1.getEntity();
byte[] buffer = new byte[40960];
entity1.getContent().read(buffer);
response.getWriter().write(new String(buffer));
} finally{
response1.close();
}
}finally{
httpclient.close();
}
return "success";
}
else
return "fail";
}

}
  • Request(对HttpClient的封装)
  • HttpURLConnection

关键代码如下,这里通过url参数接受用户的输入,然后直接openConnection打开连接:

1
2
3
4
5
6
7
8
//...

HttpServletRequest request = ServletActionContext.getRequest();
String url = request.getParameter("url");
URL obj = new URL(url);
HttpUrlConnection con = (HttpURLConnection) onj.openConnection();

//...
  • URL类
  • OKhttp

简单修复代码:

  • 白+黑+禁止访问内网ip
1
2
3
4
5
6
7
8
9
10
11
12
if(urlLink.contains("file") || urlLink.contains("gopher") || urlLink.contains("ftp"))|| urlLink.contains("netdoc")|| urlLink.contains("mailto")|| urlLink.contains("jar"){
            return "无法访问";
        }
if(urlLink.contains("127.0.0.1"))|| urlLink.contains("localhost") {
return "无法访问";
}
  ...
 
if(urlLink.contains("http://") || urlLink.contains("https://")){
            ...
            业务
        }

推荐:

禁内网ip+白名单业务:

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
31
32
33
//ipIsInner,用于判断是否为内网ip
static List<Pattern> ipFilterRegexList = new ArrayList<>();
static {

Set<String> ipFilter = new HashSet<String>();
ipFilter.add("^10\\\\.(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])"
+ "\\\\.(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])" + "\\\\.(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])$");
ipFilter.add("^172\\\\.(1[6789]|2[0-9]|3[01])\\\\" + ".(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])\\\\"
+ ".(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])$");
ipFilter.add("^192\\\\.168\\\\.(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])\\\\"
+ ".(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])$");
//ipFilter.add("127.0.0.1");
ipFilter.add("^127\\\\.(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])"
+ "\\\\.(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])" + "\\\\.(1\\\\d{2}|2[0-4]\\\\d|25[0-5]|[1-9]\\\\d|[0-9])$");
ipFilter.add("0.0.0.0");
ipFilter.add("localhost");

for (String reg : ipFilter) {
ipFilterRegexList.add(Pattern.compile(reg));
}
}

public static boolean ipIsInner(String ip) {
System.out.println(ip);
for (Pattern reg : ipFilterRegexList) {
Matcher matcher = reg.matcher(ip);
if (matcher.find()) {
return true;
}
}
return false;
}

限制协议为http或者https:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
URL exurl = new URL(urlLink);
String host = exurl.getHost();
String protocol = exurl.getProtocol();
// 协议只能是https或http
if(!(protocol.contains("https")||protocol.contains("http"))){
return "无法访问";
}
// 检查ip,不允许是内网ip
InetAddress[] addresses = InetAddress.getAllByName(host);
List<String> ips = new ArrayList<>();
for (int i = 0; i < addresses.length; i++) {
if(addresses[i] instanceof Inet4Address) {

if (ipIsInner(addresses[i].getHostAddress())) {
//System.out.println("Illegal address error");
throw new IllegalArgumentException();
} else {
ips.add(addresses[i].getHostAddress());
}
}
}

🤔,还是有点缺陷的,没有禁用100.x.x.x(企业级网关段)

fastjson

改高版本?

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version> //改这里,注释删掉
</dependency>

直接开启safemode:

1
2
ParserConfig.getGlobalInstance().setSafeMode(true);
//在代码中配置,我个人觉得这种方法比较轻松
1
2
-Dfastjson.parser.safeMode=true
//jvm启动参数
1
2
3
fastjson.properties

fastjson.parser.safeMode=true

改完java后重新打包然后重启服务,pom.xml

1
mvn clean install & mvn package

java反序列化

白名单防御:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.apache.commons.io.serialization.ValidatingObjectInputStream;
....


private static Object deserialize(byte[] buffer) throws IOException,
ClassNotFoundException , ConfigurationException {
Object obj;
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
// Use ValidatingObjectInputStream instead of InputStream
ValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais);

//只允许反序列化SerialObject class
ois.accept(SerialObject.class);
obj = ois.readObject();
return obj;
}

SerialObject是一个class数组,白名单

黑名单防御:

重写resolveClass,例如创建一个MyObjectInputStream,里面限制黑名单:

1
2
3
4
5
6
7
8
9
10
11
12
private static final String[] blacklist = new String[]{
"java\\.security.*",
"java\\.rmi.*",
"com\\.fasterxml.*",
"org\\.springframework.*",
"org\\.yaml.*",
"javax\\.management\\.remote.*",
".*TemplatesImpl.*",
".*XString.*",
".*BadAttributeValueExpException.*",
".*SignedObject.*"
};

完整代码如下,黑名单根据需要再添加:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.ctf.ezser.utils;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;


public class MyObjectInputStream extends ObjectInputStream {

private static final String[] blacklist = new String[]{
"java\\.security.*",
"java\\.rmi.*",
"com\\.fasterxml.*",
"org\\.springframework.*",
"org\\.yaml.*",
"javax\\.management\\.remote.*",
".*TemplatesImpl.*",
".*XString.*",
".*BadAttributeValueExpException.*",
".*SignedObject.*"
};

public MyObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}

protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if(!contains(cls.getName())) {
return super.resolveClass(cls);
} else {
throw new InvalidClassException("Unexpected serialized class", cls.getName());
}
}

public static boolean contains(String targetValue) {
for (String forbiddenPackage : blacklist) {
if (targetValue.matches(forbiddenPackage))
return true;
}
return false;
}
}

然后在漏洞处修改利用我们自己写的ObjectInputStream

blacklist:

1
2
3
4
5
6
7
8
9
10
11
12
org.apache.commons.collections.functors.InvokerTransformer
org.apache.commons.collections.functors.InstantiateTransformer
org.apache.commons.collections4.functors.InvokerTransformer
org.apache.commons.collections4.functors.InstantiateTransformer
org.codehaus.groovy.runtime.ConvertedClosure
org.codehaus.groovy.runtime.MethodClosure
org.springframework.beans.factory.ObjectFactory
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
org.apache.commons.fileupload
org.apache.commons.beanutils

//防常见的反序列化
1
this.denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");

直接从fastjson一致限制的黑名单denyList内copy:

GitHub - LeadroyaL/fastjson-blacklist

jspshell

啊?

d盾启动

找到就删了

不死马怎么办

我不会啊T_T

找进程吧

内存马

https://github.com/c0ny1/java-memshell-scanner

文件读取

File + FileInputStream/getPath/getAbsolutePath/ServletFileUpload

例如:

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
@GetMapping("/include/image")
public void getImage(@RequestParam String image, HttpServletResponse response) {
try {
//HttpServletResponse response = new HttpServletResponse();
//ClassPathResource classPathResource = new ClassPathResource("sql/SCHEDULE_TASK.sql");
File file = new File("resources/images/", image);
InputStream inputStream = new FileInputStream(file); //输出文件
InputStream fis = new BufferedInputStream(inputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
response.reset();

//获取文件的名字再浏览器下载页面
String name = file.getName();
response.addHeader("Content-Disposition", "attachment;filename=" + new String(name.getBytes(), "iso-8859-1"));
response.addHeader("Content-Length", "" + file.length());
OutputStream out = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
out.write(buffer);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}

简单waf:

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
public boolean validFilePath(File file) throws IOException {
if (file.getCanonicalPath().startsWith("E:\\source\\xxxxxx\\ezfastjson2\\ezfastjson2\\ezfastjson2\\ezfastjson2")){
//转绝对路径去匹配网站目录,绝对路径
return true;
}
else{
return false;
}
}


...


@GetMapping("/include/image")
public String getImage(@RequestParam String image, HttpServletResponse response) {
try {
//HttpServletResponse response = new HttpServletResponse();
//ClassPathResource classPathResource = new ClassPathResource("sql/SCHEDULE_TASK.sql");

File file = new File("resources/images/", image);

// waf
if(!validFilePath(file)){
return "ah-oh";
}

InputStream inputStream = new FileInputStream(file); //输出文件

...

只能够简单的防御,特定情况下还能够bypass

文件写

简单防御:

1
2
3
4
5
6
7
8
9
10
public boolean validFilePath(File file) throws IOException {
if (file.getCanonicalPath().startsWith("E:\\source\\huaweiCTF\\7.31\\ezfastjson2\\ezfastjson2\\ezfastjson2\\ezfastjson2")){
if(!file.getCanonicalPath().endsWith(".jsp") && !file.getCanonicalPath().endsWith(".jar") )
return true;
}
return false;

}

// 找一个路径插入

rce

对传参点进行过滤:

1
if (!Pattern.matches("[0-9A-Za-z@.]+", dir)) {

log4j2

分不同版本:

  • 2.10及以上
1
jvm参数添加 -Dlog4j2.formatMsgNoLookups=true

在应用程序的classpath下添加log4j2.component.properties配置文件:

1
log4j2.formatMsgNoLookups=True;

系统环境变量:

1
FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS,LOG4J_FORMAT_MSG_NO_LOOKUPS设置为true
  • 1.x

对于log4j 1.x版本,移除JMSAppender.class文件,命令为:zip -q -d log4j-1.x.jar org/apache/log4j/net/JMSAppender.class

注意测试,防止对业务产生影响

  • 2.x

移除JndiLookup类的方式

1
zip -q -d log4j-core-2.x.jar org/apache/log4j/core/lookup/JndiLookup.class

永久修复:

升级到最高版本log4-2.15.1-rc1

并弃用log4j 1.x版本

awd_java_web_patch

GitHub - Jlan45/AWDJavaWebPatch: 通过jar包快速生成patch模版

个人推荐的用法是不进行jar包或war包的打包,而是通过本项目生成好IDEA项目之后只进行1、2、3步骤的配置,然后通过压缩软件对编译好的class文件直接进行替换,目前测试Bandizip可以保证替换前后jar包和war包的可用性

有部分的jar包war包反编译的class是经过编译器优化的,这也导致反编译后生成的Java文件会有奇怪的语法错误,这时候对症下药即可,也可以直接删除所有java文件只保留你需要进行patch的

最后祝各位师傅在AWD赛场上玩的愉快,不被环境困扰

文件监控

1
https://github.com/TheKingOfDuck/FileMonitor

查最近十分钟被修改的文件

1
find / -name *.php -mmin -5