# ScanWebShell 开发文档
# 总体设计
后端设计如下:
| URL | 视图 | 模板 | 说明 |
| -------------------- | ------------------------ | ------------------ | ------------------------------- |
| /index/ | ScanWebShell.views.index | user/index.html | 主页与项目信息 |
| /user/login/ | user.views.login | user/login.html | 登录 |
| /user/register/ | user.views.register | user/register.html | 注册 |
| /user/logout/ | user.views.logout | 无需专门的页面 | 登出 |
| /job/upload | job.views.upload_file | job/upload.html | 上传文件 |
| /job/count | job.views.countResult | job/count.html | 显示所有任务信息 |
| /job/search/?task_id | job.views.searchResult | job/search.html | 根据 task_id 返回任务的详细信息 |
| /job/scan?file | job.views.scan_file | job/scan.html | 扫描文件 |
* index 项目信息
* user 与用户相关的处理
* login 登录
* register 登录
* forget 忘记密码
* logut 登出
* job 任务相关处理
* upload 上传 webshell 文件
* scan 对上传的文件进行扫描
* count 统计当前用户所有的扫描任务与已上传的文件
* search 具体描述某一任务,重点为改任务的结果
其中,仅 `index` 允许游客访问。
> 环境为开发环境,上传的是生产环境
# User 应用设计
## 后端设计
### 参考的项目
User 其 MVT 中的 `Module` 和 `view` 部分,参考于[基于 Django2.2 可重用登录与注册系统](https://www.liujiangblog.com/course/django/102)
模型如下:
```python
class User(models.Model):
"""
用户模型
"""
name = models.CharField(max_length=128,unique=True)
password = models.CharField(max_length=256)
email = models.EmailField(unique=True)
c_time = models.DateTimeField(auto_now_add=True)
has_confirmed = models.BooleanField(default=False)
def __str__(self):
return self.name
class Meta:
ordering = ["-c_time"]
verbose_name = "用户"
verbose_name_plural = "用户"
class ConfirmString(models.Model):
"""
邮箱确认模型
"""
code = models.CharField(max_length=256)
user = models.OneToOneField('User', on_delete=models.CASCADE)
c_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.user.name + ": " + self.code
class Meta:
ordering = ["-c_time"]
verbose_name = "确认码"
verbose_name_plural = "确认码"
```
### 其他功能
之后的 `View` 部分是在[基于 Django2.2 可重用登录与注册系统](https://www.liujiangblog.com/course/django/102)的基础上,补充部分功能:
* 忘记密码
* 重置密码
* django simple captcha refresh
#### 忘记密码
其中重置密码没有独立出来,是属于忘记密码的一部分
相关模型如下:
```python
class ConfirmString(models.Model):
"""
邮箱确认模型
"""
code = models.CharField(max_length=256)
user = models.OneToOneField('User', on_delete=models.CASCADE)
c_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.user.name + ": " + self.code
class Meta:
ordering = ["-c_time"]
verbose_name = "确认码"
verbose_name_plural = "确认码"
```
应用逻辑如下:
* 用户在 `user/forget/index` 的表单中,添加需要重置密码的用户邮箱
* 若无改用户,则弹出无该用户的警告
* 有该邮箱,则往用户邮箱发送重置密码的链接,此时
* 重置密码的链接大致为 `user/forget/confirm/?code=*`
* 当 code 是在 `ConfirmString` 实例中时,将 `user` 的 `has_confirmed`,使其在重置密码期间无法登录,之后携带 `code` 转到 `user/forget/change/?code=*`
* 若数据库中没有该 `code`,则拒绝
* `user/forget/change/?code=*` 中,根据 `code` 查询一对一匹配的 `user`,再根据添加的表单修改密码,之后 `confirm.user.save()` 和 `confirm.delete()`
#### django simple captcha refresh
在原项目基础上,需要修改 `Template` 和 `urls.py`
* `urls.py`
captcha.views 内置就有刷新验证码的方法
```python
from captcha.views import captcha_refresh # 验证码刷新功能,captcha_refresh为captcha.views内置方法,不需要我们单独写
urlpatterns = [
...
path('refresh/', captcha_refresh), # 点击可以刷新验证码
]
```
* `Template`
```html
{#刷新验证码的脚本,放到body部分的最后面即可#}
<script>
$('.captcha').click(function () {
$.getJSON('/captcha/refresh/',function (result) {
$('.captcha').attr('src',result['image_url']);
$('#id_captcha_0').val(result['key']);
});
});
</script>
```
## 前端设计
前端设计上是基本参考于 [bootstrapdoc 5.0 example](https://bootstrapdoc.com/docs/5.0/example/).
* index.html
![](https://www.writebug.com/myres/static/uploads/2022/1/6/63543ec24103ccc190244ea3fac247c6.writebug)
* login.html
![](https://www.writebug.com/myres/static/uploads/2022/1/6/eef23c1d6bc42ba2faeebbe2f6347567.writebug)
# Job 应用设计
## 后端设计
`Job` 的应用设计上,个人在设计时,分为一些几个功能:
* Upload 上传 WebShell 文件
* Count 统计当前用户的上传文件和扫描任务
* Scan 根据 `file` 文件创建扫描任务
* Search 根据 `task_id` 查询扫描任务结果
在四个任务中,`upload`、`count`、`search` 设计相对简单,网上参考也相对较多,这里只是简单介绍。而 `scan` 中的设计相对麻烦,本质上是利用 `celery` 来处理扫描任务。
### upload
在 `models` 中设计相关模型,且添加装饰器,用于在 `admin` 可以方便地同时删除文件对象和磁盘中的文件。
* `models.py`
```python
class ModelWithFileField(models.Model):
tmp_file = models.FileField(upload_to = './FileUpload/')# 上传目录为 FileUpload
file_user = models.ForeignKey(User, on_delete=models.CASCADE,null=True)
'''
值得注意的一点是,FileUpload中已经存在相同文件名的文件时,会对上传文件的文件名重命名
如 1.png 转为 1_fIZVhN3.png
且存储的文件为 1_fIZVhN3.png
'''
c_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.tmp_file.name
class Meta:
ordering = ["-c_time"]
verbose_name = "文件"
verbose_name_plural = "文件"
# 添加装饰器
@receiver(post_delete,sender=ModelWithFileField)
def delete_upload_files(sender, instance, **kwargs):
files = getattr(instance, 'tmp_file')
if not files:
return
fname = os.path.join(settings.MEDIA_ROOT, str(files))
if os.path.isfile(fname):
os.remove(fname)
```
* `views.py`
```python
def upload_file(request):
"""
上传文件
:param request:
:return:
"""
if not request.session.get('is_login', None): # 不允许重复登录
return redirect('/user/index/')
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
user_id = request.session.get('user_id')
if user_id:
tmp_user = models.User.objects.get(id=user_id)
instance = ModelWithFileField(tmp_file=request.FILES['file'], file_user=tmp_user)
instance.save()
message = "上传成功!\n存储的文件名为:\n" + instance.tmp_file.name
return render(request, 'job/upload.html', {'message_success': message})
else:
return render(request, 'job/upload.html', {'message_warning': "上传失败"