UUBlog

UUBlog

闲置一阵没折腾django后,再续上次学习进度捣鼓demo的时候发现了这.

1
list index out of range

看看报错和xadmin模块有关,但是我之前没报错,而我又没对它做过改动,一脸懵逼.

主要是在注册的数据模型中新增数据,都会报这个错.在django原生的admin模块后台是没这个错误的.

基本锁定和xadmin有关了.

遂百度 xadmin list index out of range.果然有发现,见参考资料文章.

说是django升级后出现新标签导致的这个错误.而我之前对django升级过.django 1.11.12.

文中介绍了如何修改xadmin达到目的.我懒,降级了django回 django 1.11 没毛病老铁.

参考资料

Django2集成xadmin详解-4-list index out of range报错追踪和处理

关注公众号 尹安灿

百度了一圈,没人发过,算是0day吧。

近日有朋友发了一个小说站,日流量很可观,几万IP一天。

后来就留意, 近日得到一套该站用的wap端的源码,看了下信息是个人写的。感觉可能会有漏洞,就瞄了几眼,果然发现点端倪。

首先是mysql.class.php的类。

它有个checksql()的方法,每次调用都会获取所有的GET,POST,COOKIE的参数。进行正则匹配,企图发现恶意注入的SQL内容。

但是SQL过滤不是很严,还是有绕过的空间。比如十六进制,一些注释符,过滤的sql语句比较有限。

主要内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private $getfilter = "'|(and|or)\\b.+?(>|<|=|in|like)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
private $postfilter = "\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
private $cookiefilter = "\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
function __construct(){

}
function checksql(){
foreach($_GET as $key=>$value){$this->stopattack($key,$value,$this->getfilter);}
foreach($_POST as $key=>$value){$this->stopattack($key,$value,$this->postfilter);}
foreach($_COOKIE as $key=>$value){$this->stopattack($key,$value,$this->cookiefilter);}
}
/**
* 参数检查并写日志
*/
public function stopattack($StrFiltKey, $StrFiltValue, $ArrFiltReq){
if(is_array($StrFiltValue))$StrFiltValue = implode($StrFiltValue);
if (preg_match("/".$ArrFiltReq."/is",$StrFiltValue) == 1){
$this->writeslog($_SERVER["REMOTE_ADDR"]." ".strftime("%Y-%m-%d %H:%M:%S")." ".$_SERVER["PHP_SELF"]." ".$_SERVER["REQUEST_METHOD"]." ".$StrFiltKey." ".$StrFiltValue);
exit('您提交的参数非法,系统已记录您的本次操作!');
}
}

要说构造语句添加信息或者查询信息还是不容易的。所以利用起来似乎也不容易。

在看到用户模块的时候,倒是发现新大陆了。

看修改用户资料的处理代码。
controllers/user.class.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
public function information(){
global $smartyObj;
global $db;
$db->checksql();
$step = isset($_GET["step"])?trim($_GET["step"]):"";
$info_nickname = isset($_POST["info_nickname"])?$_POST["info_nickname"]:"";
$info_emial = isset($_POST["info_emial"])?$_POST["info_emial"]:"";
$info_sex = isset($_POST["info_sex"])?$_POST["info_sex"]:"";
$info_sign = isset($_POST["info_sign"])?$_POST["info_sign"]:"";
$info_intro = isset($_POST["info_intro"])?$_POST["info_intro"]:"";
$iserror = 0;
$userinfo = $db->clogin();
if($step=="ok") {
if ($iserror == 0 and $info_nickname == "") {
$iserror = 1;
$tourl = urlconfigs::URL_auto(array("m" => "user/information"));
$pagecontent = "保存失败!<br/>";
$pagecontent .= "昵称不能为空!<br/>";
}
if ($iserror == 0) {
$a1 = $db->query_num("select uid from " . JIEQI_DB_PREFIX . "_system_users where name ='" . $info_nickname . "' and uid !=" . $userinfo->uid);
if ($a1) {
$iserror = 1;
$tourl = urlconfigs::URL_auto(array("m" => "user/information"));
$pagecontent = "保存失败!<br/>";
$pagecontent .= "该昵称已经存在!<br/>";
}
}
if ($iserror == 0 and $info_emial == "") {
$iserror = 1;

$tourl = urlconfigs::URL_auto(array("m" => "user/information"));
$pagecontent = "保存失败!<br/>";
$pagecontent .= "邮箱不能为空!<br/>";
}
if ($iserror == 0) {
$a2 = $db->query_num("select uid from " . JIEQI_DB_PREFIX . "_system_users where email ='" . $info_emial . "' and uid !=" . $userinfo->uid);
if ($a2) {
$iserror = 1;

$tourl = urlconfigs::URL_auto(array("m" => "user/information"));
$pagecontent = "保存失败!<br/>";
$pagecontent .= "该邮箱已经存在!<br/>";
}
}
if ($iserror == 0) {
if (!ereg("^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+", $info_emial)) {
$iserror = 1;

$tourl = urlconfigs::URL_auto(array("m" => "user/information"));
$pagecontent = "保存失败!<br/>";
$pagecontent .= "请填写正确的邮箱地址!<br/>";
}
}
if ($iserror == 0 and mb_strlen($info_sign, "utf-8") > 60) {
$iserror = 1;

$tourl = urlconfigs::URL_auto(array("m" => "user/information"));
$pagecontent = "保存失败!<br/>";
$pagecontent .= "您输入的签名长度超过60,请删减!<br/>";
}
if ($iserror == 0 and mb_strlen($info_intro, "utf-8") > 60) {
$iserror = 1;

$tourl = urlconfigs::URL_auto(array("m" => "user/information"));
$pagecontent = "保存失败!<br/>";
$pagecontent .= "您输入的个人简介长度超过60,请删减!<br/>";
}
}


if($iserror == 0){
if($step == "ok"){
$db->query("UPDATE ".JIEQI_DB_PREFIX."_system_users SET name='".$info_nickname."',email='".$info_emial."',sex=".$info_sex.",sign='".$info_sign."',intro='".$info_intro."' WHERE uid=".$userinfo->uid);


$tourl = urlconfigs::URL_auto(array("m"=>"user/home"));

$userinfo->name = $info_nickname;
$userinfo->email = $info_emial;
$userinfo->sex = $info_sex;
$userinfo->sign = $info_sign;
$userinfo->intro = $info_intro;

$iserror = 1;
$pagecontent = "保存成功!<br/>部分信息需要您重新登陆后才能显示最新信息哦!";
}else{
$smartyObj->assign('getuserinfo',$db->query_object("select name,email,sex,sign,intro from ".JIEQI_DB_PREFIX."_system_users where uid=".$userinfo->uid));
}
}

if($iserror == 0){

$smartydate = array("pagetitle" => "资料修改");

$tplpath = "user/information.tpl";

}else{

$smartydate = array("pagetitle" => "资料修改",
"pagecontent" => $pagecontent,
"tourl" => $tourl,
"redirecturl" => $tourl,
);

$tplpath = "public.tpl";

}
$smartyObj->Creatpage($tplpath,$smartydate);
}

info_sex性别的字段居然可以是字符串型,不转为整型,而且也没对它做长度检查。反倒是info_intro做了长度检查。
经过sqlcheck后就进入以下语句直接查询了。

1
$db->query("UPDATE ".JIEQI_DB_PREFIX."_system_users SET name='".$info_nickname."',email='".$info_emial."',sex=".$info_sex.",sign='".$info_sign."',intro='".$info_intro."' WHERE uid=".$userinfo->uid);

利用思路:
注册一个普通用户,然后利用注入修改用户组为管理员组。

修改用户信息的时候,拦截POST内容。然后修改`sex=1`为`sex=1,groupid=2`,杰奇cms是开源的,所以我能看到表字段,groupid为2的是系统管理员,有着后台所有权限。

搞定。

文件包含漏洞

发现他升级了后台,数据库备份不能用,也不能上传PHP,因为系统默认限制这个。因为是Linux系统,所以也没一些目录解析漏洞可以利用。
利用前提是magic_quotes_gpc 处于关闭状态。杰奇因为限制一定要要zend3.3.x解密,所以用的php版本是5.3以下的,
这就有了文件名截断的漏洞可以利用,有了这个,文件包含漏洞也就更加有可能了。好在经过一番寻找,发现了一个文件包含漏洞,利用起来更加方便,可以直接getshell。
就在处理动态加载脚本广告的时候。

book.class.php

1
2
3
4
5
6
7
8
9
10
11
public function ad(){
header('Content-type: text/javascript');
$addate = include_once("configs/ad/".$_GET["js"].".php");
$jsarray = explode("\n",$addate["adcontent"]);
foreach($jsarray as $jsstr) {
echo "document.write(\"";
echo str_replace("\r","",addslashes($jsstr));
echo "\");\n";
}
exit();
}

利用

简述一下吧
符合条件的,直接头像上传php一句话,后缀名为.jpg.取得图片地址.

构造url”http://domain/configs/ad/{头像相对路径}%00.php"

截断后面的php即可.

后面的你都知道了.

关注公众号 尹安灿

Django使用django-simple-captcha 生成验证码。

django-simple-captcha Github地址

安装

  1. pip install django-simple-captcha

  2. 添加captcha 到 settings.py的INSTALLED_APPSL里面。

  3. 运行python manage.py migrate 生成数据表

  4. 配置urls.py

1
2
3
urlpatterns += [
url(r'^captcha/', include('captcha.urls')),
]

引用

新建forms.py

1
2
3
4
5
6
from captcha.fields import CaptchaField

class RegisterForm(forms.Form):
email = forms.EmailField(required=True)
password = forms.CharField(required=True, min_length=6)
captcha = CaptchaField(error_messages={"invalid": u"验证码错误"})

models.py
直接验证表单是否通过,验证码字段会一并校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RegisterView(View):
def get(self, request):
register_form = RegisterForm()
return render(request=request, template_name='register.html', context={"register_form": register_form})

def post(self, request):
register_form = RegisterForm(request.POST)
if register_form.is_valid():
user_profile = UserProfile()
user_name = request.POST.get('username', '')
pass_word = request.POST.get('password', '')
user_profile.username = user_name
user_profile.email = user_name
user_profile.password = make_password(password=pass_word)
else:
return render(request=request, template_name='register.html', context={"register_form": register_form})

参考资料

Django-simple-captcha Document

关注公众号 尹安灿

form算是和一个和安全问题有关的一个东西了。

用来设置一些规则,对用户提交的字段进行一些筛选。

把不符合要求的字段过滤在外,这个一般前端也会做相应的处理。

但是有时候是可以被绕过的,比如提交正常的POST数据,然后拦截数据包,修改后,再POST。

新建<path-to-your-app>/forms.py
比如我登录认证的表单中含有username和password两个字段,那我可以定义这两个字段的规则。

它的使用方式有点像我们定义models一样,都是前面变量为关键字段名,后面跟着字段类型和属性。

以下设置了两个字段为必填项,并且对长度做了一些限制

1
2
3
4
5
6
from django import forms


class LoginForm(forms.Form):
username = forms.CharField(required=True, max_length=50)
password = forms.CharField(required=True, min_length=6)

然后可以在具体的逻辑中引用它

如我在前面一个文章中提到的登录验证中
调用它is_valid()方法来认证
views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LoginView(View):
def get(self, request):
return render(request, 'login.html')

def post(self, request):
login_form = LoginForm(request.POST)
if login_form.is_valid():
user_name = request.POST.get('username', '')
pass_word = request.POST.get('password', '')
user = authenticate(request, username=user_name, password=pass_word)
if user is not None:
login(request, user)
return render(request, 'index.html')
else:
return render(request, 'login.html', {"msg": "用户或密码错误"})
else:
return render(request, 'login.html', {"login_form": login_form})

结果传递给页面,页面可以处理,并展示它的返回错误信息之类的。

如模板中遍历它错误的值,打印出来。

1
{% for key,value in login_form.errors.items %}{{ value }}{% endfor %}

关注公众号 尹安灿

Django提供了比较方便的用户认证模块,只要导入它,就可以很方便就完成用户认证。

用户认证

主流有两种写法,一个是直接写成函数,一个写成类,重载get和post方法

  1. 用函数实现
    <path-to-your-app>/views.py
    主要的函数就两个authenticate()login()
    一个是认证登录,一个是保存登录信息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from django.contrib.auth import authenticate, login

    def login_view(request):
    if request.method == 'POST':
    user_name = request.POST.get('username', '')
    pass_word = request.POST.get('password', '')
    user = authenticate(request, username=user_name, password=pass_word)
    if user is not None:
    login(request, user)
    return render(request, 'index.html')
    else:
    return render(request, 'login.html', {"msg": "用户或密码错误"})
    if request.method == 'GET':
    return render(request, 'login.html')

然后在urls.py

1
2
3
4
5
6
7
8
9
from users.views import login_view

urlpatterns = [
url(r'^xadmin/', xadmin.site.urls),
url(r'^form/$', get_form),
url(r'^$', TemplateView.as_view(template_name="index.html"), name="index"),
url(r'^login/$', login_view),
]

  1. 用类重载get,post方法实现(推荐)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class LoginView(View):
    def get(self, request):
    return render(request, 'login.html')

    def post(self, request):
    user_name = request.POST.get('username', '')
    pass_word = request.POST.get('password', '')
    user = authenticate(request, username=user_name, password=pass_word)
    if user is not None:
    login(request, user)
    return render(request, 'index.html')
    else:
    return render(request, 'login.html', {"msg": "用户或密码错误"})
    urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from users.views import LoginView

    urlpatterns = [
    url(r'^xadmin/', xadmin.site.urls),
    url(r'^form/$', get_form),
    url(r'^$', TemplateView.as_view(template_name="index.html"), name="index"),
    # url(r'^login/$', login_view),
    url(r'^login/$', LoginView.as_view()),
    ]

    用户名或邮箱

    如果需要用户用邮箱也能登录,则重载authenticate方法就行了。
    首先定义和重载authenticate方法,调用用户模型,利用Q对象组合查询条件,实现等条件查询。这里用|,或组合查询。
    确定存在这个账户后,调用check_password对比密码,如果符合,就返回用户信息。
    <path-to-your-app>/views.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from django.contrib.auth.backends import ModelBackend
    from django.db.models import Q

    from .models import UserProfile

    # Create your views here.


    class CustomBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
    try:
    user = UserProfile.objects.get(Q(username=username) | Q(email=username))
    if user.check_password(password):
    return user
    except Exception as e:
    return None

    然后将上面的类注册到settings.py
    1
    2
    3
    AUTHENTICATION_BACKENDS = (
    'users.views.CustomBackend',
    )

done.

关注公众号 尹安灿

添加了模型,想在后台管理,使用xadmin的话,和django有一点点不一样。

注册模型到后台

在每个app下面新建一个adminx.py,导入相应的模型
xadmin.site.register(UserFavorite, UserFavoriteAdmin)

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
# _*_ coding:utf-8 _*_
import xadmin

from .models import UserMessage, UserAsk, UserCourse, UserFavorite, CourseCommets


class UserMessageAdmin(object):
list_display = ['user', 'message', 'has_read', 'add_time']
list_filter = ['user', 'message', 'has_read', 'add_time']
search_fields = ['user', 'message']


class UserAskAdmin(object):
list_display = ['name', 'mobile', 'course_name', 'add_time']
list_filter = ['name', 'mobile', 'course_name', 'add_time']
search_fields = ['name', 'mobile', 'course_name']


class UserFavoriteAdmin(object):
list_display = ['user', 'course', 'fav_id', 'fav_type', 'add_time']
list_filter = ['user', 'course', 'fav_id', 'fav_type', 'add_time']
search_fields = ['user', 'course']


class CourseCommetsAdmin(object):
list_display = ['user', 'course', 'comment', 'add_time']
list_filter = ['user', 'course', 'comment', 'add_time']
search_fields = ['user', 'course', 'comment']


class UserCourseAdmin(object):
list_display = ['user', 'course', 'add_time']
list_filter = ['user', 'course', 'add_time']
search_fields = ['user', 'course']


xadmin.site.register(UserFavorite, UserFavoriteAdmin)
xadmin.site.register(UserCourse, UserCourseAdmin)
xadmin.site.register(UserAsk, UserAskAdmin)
xadmin.site.register(UserMessage, UserMessageAdmin)
xadmin.site.register(CourseCommets, CourseCommetsAdmin)

list_display是在后台展示的列
list_filter是在后台提供过滤器的字段
search_fields是开启搜索功能的字段
最后将模型和定义的模型管理注册一些。

修改模型名字

注册到后台后,菜单那里默认显示的模型名是我们定义的英文名,要想改成中文,需要修改以下两个地方。

  1. <path-to-you-app>/apps.py
    增加verbose_name
    1
    2
    3
    4
    class OperationConfig(AppConfig):
    name = 'operation'
    verbose_name = u'用户操作'

  2. <path-to-you-app>/__init__.py
    修改默认app配置为我们指定的。
    1
    2
    default_app_config = 'operation.apps.OperationConfig'

关注公众号 尹安灿

xadmin github 地址: https://github.com/sshwsfc/xadmin

因为在使用的过程中避免不了会对xadmin做一些更改,所以,一般都是建议git clone源码到自己项目中使用。

替换django原有的admin也比较简单。

在urls.py中

1
2
3
4
5
6
import xadmin

urlpatterns = patterns('',
url(r'xadmin/', xadmin.site.urls),
)

即可

开启主题,后台站名和页脚文字修改

在app的adminx.py中重载基础设置和全局设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from xadmin import views

class BaseSettings(object):
enable_themes = True
use_bootswatch = True # 一个主题插件

class GlobalSettings(object):
site_title = '开源课程网'
site_footer = '开源课程网'
menu_style = 'accordion' # 设置菜单可收缩,手风琴 accordion


xadmin.site.register(views.BaseAdminView, BaseSettings)
xadmin.site.register(views.CommAdminView, GlobalSettings)

待持续更新…也可能再也不更新… 2018/01/21

关注公众号 尹安灿

忘记几次,还是打算记下来,虽然就三行。

1
2
3
4
5
6
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/

LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False

最后一个才是重点,否则保存时间的时候,还是使用UTC的时间。

关注公众号 尹安灿

总会遇到有些自定义的包或者自己生成的app需要放到一些特定的目录下,方便项目维护,让目录结构更加清晰。

但是把模块移动到非项目根目录下后,会遇到import错误。

这个时候需要设置下环境变量,将我们的目录加入到django运行时的环境变量中。

具体方法是,导入sys模块,插入自己的新目录地址。

我这里的样例如下:

1
2
3
4
5
6
import sys

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(BASE_DIR, 'extra_apps'))
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))

关注公众号 尹安灿

因项目需要对xadmin源码做修改,所以,没有直接使用pip安装的xadmin。卸载后,直接从github上下载了一份xadmin的最新版。

然后复制xadmin到项目当中,运行的时候发现少了一些模块。

用pip安装以下模块即可

1
pip install future six httplib2 django-import-export

2018/01/21 更新

是我瞎,其实源码路径下有个requestments.txt 安装里面的依赖包即可。

关注公众号 尹安灿

0%