add source

This commit is contained in:
lijiejie
2022-04-01 12:05:54 +08:00
parent 4c9e61f646
commit af88009d10
48 changed files with 27359 additions and 1 deletions
+1
View File
@@ -7,3 +7,4 @@ __pycache__/
venv_py
dnslog/settings_dev.py
logs/dnslog_debug.log
logview/migrations/
View File
+133
View File
@@ -0,0 +1,133 @@
# coding:utf-8
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '#ma=s-l!2obwj%h-6uu^sbw+4%i2w79%v3^ill62k3&7tjf5dc'
DEBUG = True
ALLOWED_HOSTS = ['*']
# SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'logview',
)
MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
)
ROOT_URLCONF = 'dnslog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'dnslog.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'DB_NAME',
'USER': 'DB_USER',
'PASSWORD': 'DB_PASSWORD',
'HOST': 'DB_HOST',
'PORT': '3306'
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'
if DEBUG:
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
else:
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# 用于DNS记录的域名
DNS_DOMAIN = 'eyes.sh'
# 管理后台域名
ADMIN_DOMAIN = ['eyes.sh', 'www.eyes.sh']
# NS域名
NS1_DOMAIN = 'eyes_dns1.lijiejie.com'
NS2_DOMAIN = 'eyes_dns2.lijiejie.com'
# 服务器外网地址
SERVER_IP = '123.123.123.123'
if not DEBUG:
LOGGING = {}
else:
LOG_DIR = os.path.join(BASE_DIR, 'logs')
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'verbose': {
'format': "[%(asctime)s] %(levelname)s [ %(filename)s] [line %(lineno)s] %(message)s",
'datefmt': "%d/%b/%Y %H:%M:%S"
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'file': {
'level': 'DEBUG' if DEBUG else 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOG_DIR, 'dnslog_debug.log'),
'formatter': 'verbose',
'maxBytes': 1024 * 1024 * 1024 * 2, # 2GB
'backupCount': 5,
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'DEBUG' if DEBUG else 'INFO',
'propagate': True,
},
'django.utils.autoreload': {
'level': 'CRITICAL',
}
},
}
+26
View File
@@ -0,0 +1,26 @@
# coding=utf-8
from django.urls import path, re_path
from django.contrib import admin
from logview import views
urlpatterns = [
re_path(r'^admin/login', views.index),
re_path(r'^admin/', admin.site.urls),
re_path(r'^login', views.do_login, name='login'),
re_path(r'^random_id_login', views.random_id_login, name='random_id_login'),
re_path(r'^register', views.register, name='register'),
re_path(r'^logout/', views.do_logout, name='logout'),
re_path(r'^api/group/dns/(.+?)/(.+?)/$', views.group_api, name='group_api'),
re_path(r'^api/(.+?)/(.+?)/(.+?)/$', views.api, name='api'),
re_path(r'^dns/$', views.dns_view, name='dns_view'),
re_path(r'^dns/delete$', views.dns_delete, name='dns_delete'),
re_path(r'^web/$', views.web_view, name='web_view'),
re_path(r'^web/delete$', views.web_delete, name='web_delete'),
re_path(r'^config/$', views.config_view, name='config_view'),
re_path(r'^rebind/$', views.rebind_view, name='rebind_view'),
re_path(r'^rebind/gen$', views.rebind_gen, name='rebind_gen'),
re_path(r'^as_admin/$', views.as_admin, name='as_admin'),
re_path(r'^config/update$', views.config_update, name='config_update'),
re_path(r'^.*$', views.index, name='index'),
]
+7
View File
@@ -0,0 +1,7 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnslog.settings")
application = get_wsgi_application()
+18
View File
@@ -0,0 +1,18 @@
server {
listen 80;
server_name eyes.sh;
uwsgi_pass_request_headers on;
location = /favicon.ico { access_log off; log_not_found off; }
location /static {
alias /dnslog/static;
}
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000;
}
}
View File
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+89
View File
@@ -0,0 +1,89 @@
from django.db import models
from django.contrib import admin
class User(models.Model):
id = models.AutoField(primary_key=True)
username = models.CharField(max_length=128, db_index=True)
email = models.CharField(max_length=128, db_index=True)
password = models.CharField(max_length=128, db_index=True)
user_domain = models.CharField(max_length=128, db_index=True)
token = models.CharField(max_length=32, db_index=True)
is_admin = models.BooleanField(default=0)
try_login_counter = models.IntegerField(default=0)
last_try_login_time = models.DateTimeField(auto_now=True)
login_ip = models.GenericIPAddressField(db_index=True, default='0.0.0.0')
is_random_user = models.BooleanField(default=0, db_index=True)
def __unicode__(self):
return self.username
class UserAdmin(admin.ModelAdmin):
list_display = ('username', 'password', 'user_domain')
admin.site.register(User, UserAdmin)
class WebLog(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
path = models.CharField(max_length=256, db_index=True)
remote_addr = models.GenericIPAddressField('remote_addr', db_index=True)
city = models.CharField(max_length=64, null=True, db_index=True)
user_agent = models.CharField(max_length=256, null=True, db_index=True)
created_time = models.DateTimeField(auto_now_add=True)
headers = models.TextField()
def __unicode__(self):
return self.remote_addr
class Meta:
ordering = ['-id']
class WebLogAdmin(admin.ModelAdmin):
list_display = ('user', 'path', 'remote_addr', 'user_agent', 'created_time')
admin.site.register(WebLog, WebLogAdmin)
class DNSLog(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
host = models.CharField(max_length=255, db_index=True, null=False)
type = models.CharField(max_length=8, db_index=True, null=False)
created_time = models.DateTimeField(auto_now_add=True)
ip = models.GenericIPAddressField(null=False, db_index=True)
city = models.CharField(max_length=255, null=True)
def __unicode__(self):
return self.host
class Meta:
ordering = ['-id']
class DNSLogAdmin(admin.ModelAdmin):
list_display = ('user', 'host', 'type', 'created_time')
admin.site.register(DNSLog, DNSLogAdmin)
class Config(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=128, db_index=True)
value = models.CharField(max_length=128, db_index=True)
def __unicode__(self):
return self.name
class ConfigAdmin(admin.ModelAdmin):
list_display = ('name', 'value')
admin.site.register(Config, ConfigAdmin)
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+456
View File
@@ -0,0 +1,456 @@
# coding=utf-8
import hashlib
import json
import requests
import struct
import socket
import random
import string
from django.utils import timezone
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseRedirect
from django.views.decorators.csrf import csrf_exempt
from django.core.paginator import Paginator, InvalidPage, EmptyPage, PageNotAnInteger
from django import forms
from .models import User, WebLog, DNSLog, Config
from django.contrib.auth.models import User as SysUser
from django.contrib.auth import authenticate, login
from django.conf import settings
from django.contrib.auth import logout
def get_city_by_ip(ip):
try:
doc = requests.get('http://ip.ws.126.net/ipquery?ip=%s' % ip).text
doc = doc[doc.find('var localAddress=')+17:].strip()
doc = doc.replace('city', '"city"').replace('province', '"province"')
_doc = json.loads(doc)
city = _doc['province'] + ' ' + _doc['city']
except Exception as e:
city = ''
return city
@csrf_exempt
def index(request):
http_host = request.get_host().split(':')[0]
if http_host in settings.ADMIN_DOMAIN:
return redirect('/login')
user_agent = request.META.get('HTTP_USER_AGENT') or ''
user_agent = user_agent[:250]
remote_addr = request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR')
path = http_host + request.get_full_path()
path = path[:250]
if not http_host.endswith(settings.DNS_DOMAIN):
return HttpResponse("Server Gone", status=502)
subdomain = http_host.replace(settings.DNS_DOMAIN, '')
if subdomain:
items = subdomain.split('.')
if len(items) >= 2:
user_domain = items[-2]
user = User.objects.filter(user_domain=user_domain)
if user:
city = get_city_by_ip(remote_addr)
request_headers = ''
for key in request.META:
if key.startswith('REQUEST_') or key.startswith('HTTP_') or key.startswith('CONTENT_'):
request_headers += key + ': ' + str(request.META[key]) + '\n'
if request.META.get('REQUEST_METHOD') == 'POST':
request_headers += '\n' + request.body.decode()
weblog = WebLog(user=user[0], path=path, remote_addr=remote_addr,
user_agent=user_agent, city=city, headers=request_headers)
weblog.save()
if path.find('/rpb.png') >= 0: # this is for AWVS Scanner
return HttpResponse('39a6ea3246b507782676a6d79812fa1d29e12e9c')
return HttpResponse('OK')
return HttpResponse('Failed')
class UserForm(forms.Form):
username = forms.CharField(label='用户名', max_length=128)
password = forms.CharField(label='密码', widget=forms.PasswordInput())
def do_login(request):
userid = request.session.get('userid', None)
if userid:
return HttpResponseRedirect('/dns/')
context = {'title': '登录', 'enable_register': 'yes', 'enable_create_random_user': 'yes'}
c = Config.objects.filter(name='enable_register')
if c and 'no' == c[0].value:
context['enable_register'] = 'no'
c = Config.objects.filter(name='enable_create_random_user')
if c and 'no' == c[0].value:
context['enable_create_random_user'] = 'no'
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
user = User.objects.filter(username=username)
if user:
user = user[0]
if user.try_login_counter > 10 and (timezone.now() - user.last_try_login_time).seconds < 600:
user.try_login_counter += 1
user.save()
context['error_msg'] = '登录失败次数太多,锁定10分钟'
return render(request, 'login.html', context)
if user.password == hashlib.md5((password + username[:3] + '@dnslog').encode('utf-8')).hexdigest():
request.session['userid'] = user.id
request.session['username'] = user.username
user.try_login_counter = 0
user.login_ip = request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR')
user.save()
return redirect('/dns/')
else:
user.try_login_counter += 1
user.save()
context['error_msg'] = '用户名或密码错误'
return render(request, 'login.html', context)
if User.objects.count() < 1:
return redirect('/register')
context['error_msg'] = request.GET.get('msg', '')
return render(request, 'login.html', context)
def random_id_login(request):
c = Config.objects.filter(name='enable_create_random_user')
if c and 'no' == c[0].value:
return redirect('/login?msg=随机账号功能已关闭')
user_agent = request.META.get('HTTP_USER_AGENT').lower()
if user_agent.find('bot') > 0 or user_agent.find('spider') > 0:
return redirect('/login?msg=爬虫禁止登录')
user_ip = request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR')
if User.objects.filter(login_ip=user_ip).count() > 20:
return redirect('/login?msg=随机账号超限,请注册登录')
for _ in range(30):
username = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
if not User.objects.filter(username=username):
token = hashlib.md5((username + '@random_token').encode('utf-8')).hexdigest()[:8]
user = User.objects.create(username=username, password='NO_LOGIN', email='', user_domain=username,
token=token, try_login_counter=0, login_ip=user_ip, is_random_user=1)
request.session['userid'] = user.id
request.session['username'] = username
return redirect('/dns/')
return redirect('/login?msg=随机账号创建失败')
def register(request):
c = Config.objects.filter(name='enable_register')
if c and 'no' == c[0].value:
return redirect('/login?msg=注册功能已关闭')
username = request.POST.get('username', '')[:100]
password = request.POST.get('password', '')[:100]
email = request.POST.get('email', '')[:100]
context = {'title': '注册', 'reg': 'true', 'error_msg': '',
'username': username, 'password': password, 'email': email}
if User.objects.count() < 1:
context['init'] = 'true'
if request.method == 'POST':
if User.objects.filter(username=username):
context['error_msg'] = '无法创建该账号,已被占用'
return render(request, 'login.html', context)
else:
password = hashlib.md5((password + username[:3] + '@dnslog').encode('utf-8')).hexdigest()
token = hashlib.md5((password + username[:3] + '@token').encode('utf-8')).hexdigest()[:8]
user = User.objects.create(username=username, password=password,
email=email, user_domain=username, token=token, try_login_counter=0)
if context.get('init', ''):
user.is_admin = 1
SysUser.objects.create_user(username=username, email=email, password=password,
is_staff=1, is_superuser=1)
user.save()
return redirect('/login?msg=注册成功,请登录以确保密码有效')
return render(request, 'login.html', context)
def do_logout(request):
logout(request)
return HttpResponseRedirect('/')
def get_page(p):
try:
page = int(p)
if page < 1:
page = 1
except ValueError:
page = 1
return page
def dns_view(request):
return log_view(request, 'dns')
def dns_delete(request):
userid = request.session.get('userid', None)
if not userid:
return redirect('/login')
dns_id = request.GET.get("id")
ip = request.GET.get("ip")
domain = request.GET.get("domain")
user = User.objects.filter(id=userid)[0]
obj = DNSLog.objects.filter(user=user)
if dns_id:
obj = obj.filter(id=dns_id)
if ip:
obj = obj.filter(ip=ip)
if domain:
obj = obj.filter(host__icontains=domain)
url = '/dns/?page=' + request.GET.get("page") if request.GET.get("page") else '/dns/?'
if obj:
if user.username != 'demo':
obj.delete()
return redirect(url+'&suc=删除成功')
else:
if request.GET.get("domain"):
url += "&domain=" + request.GET.get("domain")
if request.GET.get("ip"):
url += "&ip=" + request.GET.get("ip")
return redirect(url+"&fail=删除失败")
def web_delete(request):
userid = request.session.get('userid', None)
if not userid:
return redirect('/login')
web_id = request.GET.get("id")
ip = request.GET.get("ip")
path = request.GET.get("path")
headers = request.GET.get("headers")
user = User.objects.filter(id=userid)[0]
obj = WebLog.objects.filter(user=user)
if web_id:
obj = obj.filter(id=web_id)
if ip:
obj = obj.filter(remote_addr=ip)
if path:
obj = obj.filter(path__icontains=path)
if headers:
obj = obj.filter(headers__icontains=headers)
url = '/web/?page=' + request.GET.get("page") if request.GET.get("page") else '/web/?'
if obj:
if user.username != 'demo':
obj.delete()
return redirect(url+'&suc=删除成功')
else:
if request.GET.get("domain"):
url += "&domain=" + request.GET.get("domain")
if request.GET.get("ip"):
url += "&ip=" + request.GET.get("ip")
return redirect(url+"&fail=删除失败")
def web_view(request):
return log_view(request, 'web')
def config_view(request):
return log_view(request, 'config')
def rebind_view(request):
return log_view(request, 'rebind')
def log_view(request, type):
userid = request.session.get('userid', None)
if not userid:
return redirect('/login')
user = User.objects.filter(id=userid)[0]
context = {}
context['is_admin'] = user.is_admin
context['type'] = type
for var_name in ['domain', 'suc', 'fail', 'monitor', 'path', 'headers']:
context[var_name] = request.GET.get(var_name, '')
page = get_page(request.GET.get("page", 1))
if type == 'dns':
objects = DNSLog.objects.filter(user=user)
if request.GET.get('ip'):
objects = objects.filter(ip=request.GET.get('ip'))
if request.GET.get('domain'):
objects = objects.filter(host__icontains=request.GET.get('domain'))
paginator = Paginator(objects.order_by('-id'), 10)
try:
logs = paginator.page(page)
except(EmptyPage, InvalidPage, PageNotAnInteger):
page = paginator.num_pages
logs = paginator.page(paginator.num_pages)
if request.GET.get('check_update', '') == 'true':
if request.GET.get('last_id') == str(logs[0].id):
return HttpResponse('noChange')
else:
return HttpResponse('Changed')
context['title'] = 'DNSLog管理'
context['page'] = page
context['logs'] = logs
context['numpages'] = paginator.num_pages
context['total'] = paginator.count
query_prefix = ''
if request.GET.get('ip'):
query_prefix += 'ip=' + request.GET.get('ip') + '&'
if request.GET.get('domain'):
query_prefix += 'domain=' + request.GET.get('domain') + '&'
context['query_prefix'] = query_prefix
context['last_id'] = logs[0].id if logs else 0
elif type == 'web':
objects = WebLog.objects.filter(user=user)
if request.GET.get('ip'):
objects = objects.filter(remote_addr=request.GET.get('ip'))
if request.GET.get('path'):
objects = objects.filter(path__icontains=request.GET.get('path'))
if request.GET.get('headers'):
objects = objects.filter(headers__icontains=request.GET.get('headers'))
paginator = Paginator(objects.order_by('-id'), 10)
try:
logs = paginator.page(page)
except(EmptyPage, InvalidPage, PageNotAnInteger):
page = paginator.num_pages
logs = paginator.page(paginator.num_pages)
if request.GET.get('check_update', '') == 'true':
if request.GET.get('last_id') == str(logs[0].id):
return HttpResponse('noChange')
else:
return HttpResponse('Changed')
context['title'] = 'WebLog管理'
context['page'] = page
context['logs'] = logs
context['numpages'] = paginator.num_pages
context['total'] = paginator.count
query_prefix = ''
if request.GET.get('ip', ''):
query_prefix += 'ip=' + request.GET.get('ip') + '&'
if request.GET.get('path', ''):
query_prefix += 'path=' + request.GET.get('path') + '&'
if request.GET.get('headers', ''):
query_prefix += 'headers=' + request.GET.get('headers') + '&'
context['query_prefix'] = query_prefix
context['last_id'] = logs[0].id if logs else 0
elif type == 'config':
context['enable_register'] = 'yes'
context['enable_create_random_user'] = 'yes'
if user.is_admin:
for c_name in ['enable_register', 'enable_create_random_user']:
c = Config.objects.filter(name=c_name)
if c and 'no' == c[0].value:
context[c_name] = 'no'
context['title'] = 'API配置'
context['last_id'] = context['total'] = context['page'] = 0
context['query_prefix'] = ''
context['host'] = request.scheme + '://' + request.get_host()
elif type == 'rebind':
context['title'] = 'DNS重绑定'
context['last_id'] = 0
context['total'] = 0
context['page'] = 0
context['query_prefix'] = ''
context['host'] = request.scheme + '://' + request.get_host()
else:
return HttpResponseRedirect('/')
context['userdomain'] = user.user_domain + '.' + settings.DNS_DOMAIN
context['token'] = user.token
context['username'] = user.username
context['admin_domain'] = str(settings.ADMIN_DOMAIN)
return render(request, 'views.html', context)
def api(request, type, username, prefix):
result = False
token = request.GET.get('token', '')
if not User.objects.filter(username=username, token=token):
return HttpResponse('Invalid token')
host = "%s.%s.%s" % (prefix, username, settings.DNS_DOMAIN)
if type == 'dns':
res = DNSLog.objects.filter(host=host)
if len(res) > 0:
result = True
elif type == 'web':
res = WebLog.objects.filter(path__contains=host)
if len(res) > 0:
result = True
else:
return HttpResponseRedirect('/')
return HttpResponse(result)
def group_api(request, username, prefix):
token = request.GET.get('token', '')
if not User.objects.filter(username=username, token=token):
return HttpResponse('Invalid token')
postfix = ".%s.%s.%s" % (prefix, username, settings.DNS_DOMAIN)
res = DNSLog.objects.filter(host__endswith=postfix).order_by('host').values('host').distinct()
if res:
res = res[:50]
data = [item['host'].replace(postfix, '') for item in res]
text = json.dumps({"success": "true", "data": data})
return HttpResponse(text, content_type="application/json")
else:
return HttpResponse("False")
def as_admin(request):
userid = request.session.get('userid', None)
if not userid:
return redirect('/login')
user = User.objects.filter(id=userid)[0]
if user.is_admin:
su = SysUser.objects.filter(username=user.username, is_superuser=1)
if su:
request.user = su[0]
login(request, su[0])
return redirect('/admin/')
return redirect('/')
def config_update(request):
userid = request.session.get('userid', None)
config_name = request.GET.get('name', '')
config_value = request.GET.get('value', '')
if not userid or config_value not in ('yes', 'no') or \
config_name not in ['enable_register', 'enable_create_random_user']:
return HttpResponse('Fail', status=403)
user = User.objects.filter(id=userid)[0]
if user.is_admin:
c = Config.objects.filter(name=config_name)
if c:
c[0].value = config_value
c[0].save()
else:
Config.objects.create(name=config_name, value=config_value)
return HttpResponse('Updated')
return HttpResponse('Fail', status=403)
def rebind_gen(request):
userid = request.session.get('userid', None)
if not userid:
return HttpResponse('Fail', status=403)
valid_ip = request.GET.get('valid_ip', '')
invalid_ip = request.GET.get('invalid_ip', '')
try:
valid_ip = hex(struct.unpack("!I", socket.inet_aton(valid_ip))[0])[2:]
invalid_ip = hex(struct.unpack("!I", socket.inet_aton(invalid_ip))[0])[2:]
user_domain = request.session.get('username') + '.' + settings.DNS_DOMAIN
return HttpResponse('%s.%s.r.%s' % (valid_ip, invalid_ip, user_domain))
except Exception as e:
return HttpResponse('Fail')
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env python
import os
import sys
import multiprocessing
import zoneresolver
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnslog.settings")
from django.core.management import execute_from_command_line
if len(sys.argv) >= 2 and sys.argv[1] == 'runserver':
p = multiprocessing.Process(target=zoneresolver.main)
p.daemon = True
p.start()
execute_from_command_line(sys.argv)
+5
View File
@@ -0,0 +1,5 @@
Django==4.0.3
mysqlclient==2.1.0
dnslib==0.9.19
requests
gunicorn
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+325
View File
@@ -0,0 +1,325 @@
/*!
* Bootstrap Reboot v4.6.1 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([class]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg {
overflow: hidden;
vertical-align: middle;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
[role="button"] {
cursor: pointer;
}
select {
word-wrap: normal;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.6.1 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

+2
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+42
View File
@@ -0,0 +1,42 @@
html,
body {
height: 100%;
}
body {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 430px;
padding: 15px;
margin: auto;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
+6
View File
@@ -0,0 +1,6 @@
<div style="margin-left: auto;">
<label><input type="checkbox" class="form-control ml-5 mr-2" id="auto_refresh" {% if monitor == 'true' %} checked {% endif %}>监视刷新</label>
</div>
<div class="mr-5 ml-2">
<img src="/static/help.png" width="17%" height="17%" data-toggle="popover" data-trigger="hover" data-title="自动刷新" data-content="监视频率为20秒,一旦发现新域名,本页面将自动刷新。如果希望监视特定关键词,请在搜索结果页开启"/>
</div>
+76
View File
@@ -0,0 +1,76 @@
{% load static %}
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ title }}</title>
<link rel="shortcut icon" href="/static/favicon.png"/>
<link href="{% static 'bootstrap-4.6.1/css/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'signin.css' %}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" method="post"{% if reg == 'true' %} oninput='password_check.setCustomValidity(password_check.value != password.value ? "两次输入的密码不一致" : "")'{% endif %}>
<img class="mb-4" src="{% static "pentester.png" %}" width="400">
<div class="alert-warning mb-2">{{ error_msg }}</div>
{% if reg == 'true' %}
<p class="alert alert-info">{% if init == 'true' %}<span style="color: red">请创建第一个账户,默认将成为管理员</span><br />{% endif %}用户名即域名前缀,越短,越有利于绕过限制</p>
<input type="text" id="username" name="username" class="form-control mt-3" placeholder="用户名" value="{{ username }}" required autofocus>
<input type="email" id="email" name="email" class="form-control" placeholder="邮箱" value="{{ email }}" style="border-radius: 0;margin-bottom: -1px;" required autofocus>
<input type="password" id="password" name="password" class="form-control" placeholder="密码" minlength="6" value="{{ password }}" style="border-radius: 0;margin-bottom: -1px;" required>
<input type="password" id="password_check" name="password_check" class="form-control" placeholder="再次输入密码" required>
{% csrf_token %}
<button class="btn btn-lg btn-primary btn-block" type="submit">创建用户</button>
<div class="mt-4"><a href="/login">已有账号?登录</a></div>
{% else %}
<input type="text" id="username" name="username" class="form-control" placeholder="用户名" required autofocus>
<input type="password" id="password" name="password" class="form-control" placeholder="密码" minlength="6" required>
{% csrf_token %}
<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
<div class="mt-4">
{% if enable_register == 'yes' %}<a href="/register">注册新账号</a> {% endif %}
{% if enable_create_random_user == 'yes' %}
<a href="#" class="ml-5" onclick="gen_random_id(event, false)">创建随机ID并登录</a>
{% endif %}</div>
{% endif %}
<div class="alert alert-info mt-5" role="alert">
基于<a href="https://github.com/bugscanteam/dnslog/" class="alert-link" target="_blank">bugscan dnslog</a>开发<br/>
</div>
<p class="mt-5 mb-3 text-muted">&copy; 2022-2022 <a href="https://github.com/lijiejie/eyes.sh" target="_blank">LiJieJie</a></p>
</form>
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">提示</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>随机账号创建后,无法再次登录,其数据可能被定期清理 <br />
如果你希望长期保存日志,可注册个人账号</p>
</div>
<div class="modal-footer">
<a class="button btn-sm btn-primary" style="padding-top: 7px;" href="#"
onclick="gen_random_id(event, true)">确认创建</a>
</div>
</div>
</div>
<script src="{% static 'jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'bootstrap-4.6.1/js/bootstrap.bundle.js' %}"></script>
<script>
function gen_random_id(e, confirm) {
e.preventDefault();
if (confirm){
window.location.replace('/random' + '_id' + '_login');
}
$('#confirmModal').modal();
}
</script>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
<nav>
<ul class="pagination">
{% if logs.has_previous %}
<li class="page-item"><a class="page-link" href="?page={{ logs.previous_page_number }}">
<span aria-hidden="true">&laquo;</span></a></li>
{% else %}
<li class="page-item disabled"><span class="page-link" aria-hidden="true">&laquo;</span></li>
{% endif %}
{% if logs.number|add:'-4' > 1 %}
<li class="page-item"><a class="page-link" href="?{{ query_prefix }}page={{ logs.number|add:'-5' }}">&hellip;</a></li>
{% endif %}
{% for i in logs.paginator.page_range %}
{% if logs.number == i %}
<li class="page-item active"><span class="page-link">{{ i }} <span class="sr-only">(current)</span></span></li>
{% elif i > logs.number|add:'-5' and i < logs.number|add:'5' %}
<li class="page-item"><a class="page-link" href="?{{ query_prefix }}page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if logs.paginator.num_pages > logs.number|add:'4' %}
<li class="page-item"><a class="page-link" href="?{{ query_prefix }}page={{ logs.number|add:'5' }}">&hellip;</a></li>
{% endif %}
{% if logs.has_next %}
<li class="page-item"><a class="page-link" href="?{{ query_prefix }}page={{ logs.next_page_number }}"><span aria-hidden="true">&raquo;</span></a></li>
{% else %}
<li class="page-item disabled"><span class="page-link" aria-hidden="true">&raquo;</span></li>
{% endif %}
<span class="page-link ml-4" style="border: none">第{{ page }}页 / 共{{ numpages }}页, 共{{ total }}条记录</span>
<a class="button btn-sm btn-danger" style="padding-top: 7px;" href="delete?{{ query_prefix }}{% if page != 1 %}&page={{ page }}{% endif %}"
onclick="delete_all(event, false)">删除所有记录</a>
</ul>
</nav>
+389
View File
@@ -0,0 +1,389 @@
{% load static %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<link rel="shortcut icon" href="/static/favicon.png"/>
<link href="{% static 'bootstrap-4.6.1/css/bootstrap.min.css' %}" rel="stylesheet">
<style>
pre {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
mark {
background-color: #e3f2fd;
color: green;
}
.card-header {
font-weight: bold;
}
</style>
</head>
<body style="padding-top:10px;">
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light mb-3" style="background-color: #e3f2fd;">
<ul class="nav nav-pills mr-auto">
<li class="nav-item mr-3" ><img src="/static/icon.png" height="40"/></li>
<li class="nav-item" ><a class="nav-link{% if type == 'dns' %} active{% endif %}" href="/dns/">DNSLog</a></li>
<li class="nav-item"><a class="nav-link{% if type == 'web' %} active{% endif %}" href="/web/">WebLog</a></li>
<li class="nav-item"><a class="nav-link{% if type == 'config' %} active{% endif %}" href="/config/">API</a></li>
<li class="nav-item"><a class="nav-link{% if type == 'rebind' %} active{% endif %}" href="/rebind/">Rebind</a></li>
{% if is_admin %}<li class="nav-item"><a class="nav-link" href="/as_admin/">Admin</a></li>{% endif %}
</ul>
<ul class="navbar-nav">
<span class="mr-3" style="color: cornflowerblue">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
{{ username }}</span>
<a href="/logout/">退出</a>
</ul>
</nav>
{% if fail %}<div class="alert alert-danger">{{ fail }}</div>{% endif %}
{% if suc %}<div class="alert alert-success">{{ suc }}</div>{% endif %}
{% if type == 'dns'%}
<form class="form-inline my-2 mt-md-0 input-group-sm" style="display: flex;justify-content: space-between">
<div>
<input class="form-control mr-sm-2 input-sm" size="30" type="text" placeholder="域名" name="domain" value="{{ domain }}">
<button class="btn btn-sm btn-primary my-2 my-sm-0" type="submit">搜索</button>
</div>
{% include 'auto_reload.html' %}
</form>
<div data-example-id="hoverable-table">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>域名</th>
<th>Type</th>
<th>IP</th>
<th>位置</th>
<th>时间</th>
<th style="width: 100px;">操作</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<th scope="row">{{log.id}}</th>
<td style="word-break:break-all">{{ log.host }}</td>
<td>{{ log.type }}</td>
<td><a href="?ip={{ log.ip }}">{% if log.ip %} {{ log.ip }}</a> {% else %} &nbsp; {% endif %} </td>
<td>{% if log.city %} {{ log.city }} {% else %} &nbsp; {% endif %}</td>
<td>{{ log.created_time|date:'Y-m-d H:i:s' }}</td>
<td>
<a class="button btn-sm btn-danger" href="delete?{{ query_prefix }}id={{ log.id }}{% if page != 1 %}&page={{ page }}{% endif %}">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include 'pager.html' %}
</div>
{% endif %}
{% if type == 'web'%}
<form class="form-inline my-2 mt-md-0 input-group-sm" style="display: flex;justify-content: space-between">
<div>
<select class="form-control mr-2">
<option {% if path != '' or headers == '' %}selected{% endif %}>URL</option>
<option {% if headers != '' and path == '' %}selected{% endif %}>Headers</option>
</select>
<input class="form-control mr-sm-2 input-sm {% if headers != '' and path == '' %}d-none{% endif %}" size="30" type="text" placeholder="URL" name="path" id="inputURL" value="{{ path }}">
<input class="form-control mr-sm-2 input-sm {% if path != '' or headers == '' %}d-none{% endif %}" size="30" type="text" placeholder="headers" name="headers" id="inputHeaders" value="{{ headers }}">
<button class="btn btn-sm btn-primary my-2 my-sm-0" type="submit">搜索</button>
</div>
{% include 'auto_reload.html' %}
</form>
<div data-example-id="hoverable-table">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th style="width: 350px;">URL</th>
<th>IP</th>
<th>位置</th>
<th>时间</th>
<th style="width: 150px;">操作</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<th scope="row">{{log.id}}</th>
<td> <span style="word-break:break-all">{{ log.path }} </span></td>
<td><a href="?ip={{ log.remote_addr }}">{% if log.remote_addr %} {{ log.remote_addr }}</a> {% else %} &nbsp; {% endif %}</td>
<td> {% if log.city %} {{ log.city }} {% else %} &nbsp; {% endif %} </td>
<td>{{ log.created_time|date:'Y-m-d H:i:s' }}</td>
<td style="width: 150px;">
<a type="button" class="button btn-sm btn-primary" onclick="show_web_headers({{log.id}})" id="showButton_{{log.id}}">查看</a>
<a type="button" class="button btn-sm btn-danger" href="delete?{{ query_prefix }}id={{ log.id }}{% if page != 1 %}&page={{ page }}{% endif %}">删除</a>
</td>
</tr>
<tr class="d-none" id="web_headers_{{log.id}}">
<td colspan="6" style="word-break:break-all">
<div>
<pre class="alert alert-info">{% if log.city %}{{ log.headers }}{% else %}{{ log.remote_addr }}{% endif %}</pre>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include 'pager.html' %}
</div>
{% endif %}
{% if type == 'config'%}
<div class="mt-3">
<div class="card">
<div class="card-body">
<div class="card" >
<div class="card-body">
<p class="card-text">
<b>子域名:</b> <mark>{{userdomain}}</mark>
<b class="ml-4">Token:</b> <mark>{{token}}</mark>
{% if is_admin %}
<b class="ml-4 config"><input type="checkbox" id="enable_register" {% if enable_register == 'yes' %}checked{% endif %}> 开启用户注册功能</b>
<b class="ml-4 config"><input type="checkbox" id="enable_create_random_user" {% if enable_create_random_user == 'yes' %}checked{% endif %}> 开启产生随机账号</b>
{% endif %}
</p>
</div>
</div>
<div class="card mt-2">
<div class="card-header">
单个请求命中查询
</div>
<div class="card-body">
<p class="card-text">
{{host}}/api/{type}/{{username}}/{prefix}/?token={{ token }}</p>
</p>
<ul>
<li>type: 设置为 <mark>dns</mark> 或者 <mark>web</mark></li>
<li>prefix: 三级域名前缀</li>
</ul>
<p>检查是否触发 <mark>test.{{userdomain}}</mark>,访问:<a href="{{host}}/api/dns/{{username}}/test/?token={{ token }}" target="_blank">{{host}}/api/dns/{{username}}/test/?token={{ token }}</a></p>
<ul>
<li>返回 <mark>True</mark>,请求被触发</li>
<li>返回 <mark>False</mark>,请求未触发</li>
</ul>
</div>
</div>
<div class="card mt-2">
<div class="card-header">
多个请求命中查询
</div>
<div class="card-body">
<p class="card-text">
{{host}}/api/group/dns/{{username}}/{prefix}/?token={{ token }}</p>
</p>
<ul>
<li>prefix: 三级域名前缀</li>
</ul>
<p>对于一个较大的Payload集合,首先批量投递payload,再通过单个请求,来检查哪些payload触发执行成功。</p>
<ul>
<li>扫描器投递 <mark>payload1</mark>,使用域名:<mark>payload1.xxx-rce.{{userdomain}}</mark></li>
<li>扫描器投递 <mark>payload2</mark>,使用域名:<mark>payload2.xxx-rce.{{userdomain}}</mark></li>
<li>扫描器投递 <mark>payload3</mark>,使用域名:<mark>payload3.xxx-rce.{{userdomain}}</mark></li>
<li>...</li>
</ul>
<p>检查是否存在漏洞,访问:<a href="{{host}}/api/group/dns/{{username}}/xxx-rce/?token={{ token }}" target="_blank">{{host}}/api/group/dns/{{user_domain}}/xxx-rce/?token={{ token }}</a></p>
<ul>
<li>返回 <mark>False</mark>,漏洞未触发</li>
<li>返回 <mark>{"success": "true", "data": ["payload3", "payload2", "payload1"]}</mark>,漏洞被触发,data字段包含成功触发的payload清单,至多返回50个</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if type == 'rebind'%}
<div class="mt-3">
<div class="card">
<div class="card-body">
<div class="card">
<div class="card-header">
生成域名
</div>
<div class="card-body">
<p class="card-text">
<form class="form-inline my-2 mt-md-0 input-group-sm" style="display: flex;justify-content: space-between">
<div>
<input class="form-control mr-sm-2 input-sm" size="20" type="text" placeholder="合法IP" id="valid_ip" value="123.123.123.123">
<input class="form-control mr-sm-2 input-sm" size="20" type="text" placeholder="非法IP" id="invalid_ip" value="10.1.1.1">
<button class="btn btn-primary my-2 my-sm-0" type="button" onclick="generate_domain(event)">生成域名</button>
</div>
</form>
<div class="mt-4" id="result" style="color: red"></div>
<div class="mt-4"><a href="/dns/?domain=.r.{{ userdomain }}&monitor=true" target="_blank" class="mr-4">DNS监控</a>
<a href="/web/?path=.r.{{ userdomain }}&monitor=true" target="_blank">HTTP监控</a></div>
</p>
</div>
</div>
<div class="card mt-2">
<div class="card-header">
什么是DNS重绑定
</div>
<div class="card-body">
<p class="card-text">
应用程序在请求用户可控的URL/域名资源时,有安全经验的程序员,往往会先解析域名,判断域名指向的IP是否在允许的范围中</p>
<p class="card-text">
例如,为了防御SSRF攻击,开发会首先检测URL中的域名指向的是一个非内网IP,才会在应用中发起后续请求。但是,由于第二步HTTP请求并不直接依赖第一步DNS请求的结果,黑客可以控制第二步请求中的目标IP,导致绕过检测。
</p>
<p class="card-text">
攻击的方法是:<br />
<mark>在程序做合法性校验时,DNS服务器首先返回一个预期的合法IP。由于DNS记录TTL为0,当应用真正发起HTTP请求资源时,再次DNS查询,此时DNS服务器立即返回一个非法的IP。</mark>
</p>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">提示</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
是否清空全部 <b>{{ total }}</b> 条记录?删除后数据无法恢复
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
<a class="button btn-sm btn-danger" style="padding-top: 7px;" href="delete?{{ query_prefix }}{% if page != 1 %}&page={{ page }}{% endif %}"
onclick="delete_all(event, true)">删除</a>
</div>
</div>
</div>
</div>
<script src="{% static 'jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'bootstrap-4.6.1/js/bootstrap.bundle.js' %}"></script>
<script>
function show_web_headers(id) {
if ( $('#web_headers_' + id)[0].className == 'd-none'){
$('#web_headers_' + id).removeClass('d-none');
$('#showButton_' + id).text('收起');
} else {
$('#web_headers_' + id).addClass('d-none');
$('#showButton_' + id).text('查看');
}
}
/* var getUrlParameter = function getUrlParameter(sParam) {
var sPageURL = window.location.search.substring(1),
sURLVariables = sPageURL.split('&'),
sParameterName,
i;
for (i = 0; i < sURLVariables.length; i++) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1]);
}
}
return false;
};
*/
function delete_all(e, confirm) {
if (confirm)
return true;
e.preventDefault();
$('#confirmModal').modal();
/* if (!getUrlParameter('ip') && !getUrlParameter('domain') && !getUrlParameter('path') && !getUrlParameter('headers')) {
e.preventDefault();
$('#confirmModal').modal();
}
*/
}
$('select').on('change', function() {
if (this.value=='URL') {
$('#inputHeaders').prop('value', '');
$('#inputHeaders').addClass('d-none');
$('#inputURL').removeClass('d-none');
} else {
$('#inputHeaders').removeClass('d-none');
$('#inputURL').prop('value', '');
$('#inputURL').addClass('d-none');
}
});
{% if monitor %}
window.reload_interval = setInterval(data_changed, 10000);
{% endif %}
$('#auto_refresh').change(function () {
if ($('#auto_refresh').is(':checked')){
window.reload_interval = setInterval(data_changed, 10000);
} else {
clearInterval(window.reload_interval);
}
})
function data_changed() {
let base_url = $(location).attr('href');
if (base_url.indexOf('?') < 0){
base_url += '?'
}
url = base_url + '&check_update=true&last_id=' + {{ last_id }};
$.ajax({url:url, success:function(resp){
if (resp === 'Changed'){
if (base_url.indexOf('monitor=true') < 0){
base_url += '&monitor=true'
}
window.location.replace(base_url);
}
}});
}
function generate_domain(e){
e.preventDefault();
url = '/rebind/gen?valid_ip=' + $('#valid_ip').prop('value') + '&invalid_ip=' + $('#invalid_ip').prop('value') ;
$.ajax({url:url, success:function(resp){
$('#result').html(resp);
}});
}
$('.config').change(function (e) {
url = '/config/update?name=' + e.target.id + '&value=';
if ($('#' + e.target.id).is(':checked')){
$.ajax({url:url+'yes', success:function(resp){
location.reload();
}});
} else {
$.ajax({url:url+'no', success:function(resp){
location.reload();
}});
}
})
$(function () {
$('[data-toggle="popover"]').popover();
})
</script>
</body>
</html>
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnslog.settings")
import django
django.setup()
import copy
import re
import json
import requests
import struct
import socket
import random
from dnslib import RR, QTYPE, RCODE, TXT, A
from dnslib.server import DNSServer, DNSHandler, BaseResolver, DNSLogger
from logview.models import *
from dnslog import settings
class MySQLLogger:
def __init__(self):
pass
def log_data(self, dnsobj):
pass
def log_error(self, handler, e):
pass
def log_pass(self, *args):
pass
def log_prefix(self, handler):
pass
def log_recv(self, handler, data):
pass
def log_reply(self, handler, reply):
pass
def log_request(self, handler, request):
if QTYPE[request.q.qtype] == 'AAAA':
return
domain = request.q.qname.__str__().lower()
if not domain.endswith(settings.DNS_DOMAIN + '.'):
return
matches = re.search(r'\.?([^\.]+)\.%s\.' % settings.DNS_DOMAIN, domain)
if not matches:
return
user = User.objects.filter(user_domain__exact=matches.group(1))
if not user and domain.strip(".") != settings.ADMIN_DOMAIN:
user = User.objects.filter(user_domain__exact='@')
if not user:
return
ip = handler.client_address[0]
try:
doc = requests.get('http://ip.ws.126.net/ipquery?ip=%s' % ip).text
doc = doc[doc.find('var localAddress=')+17:].strip()
doc = doc.replace('city', '"city"').replace('province', '"province"')
_doc = json.loads(doc)
city = _doc['province'] + ' ' + _doc['city']
except Exception as e:
city = ''
log = DNSLog(user=user[0], host=domain.strip('.'), type=QTYPE[request.q.qtype], ip=ip, city=city)
log.save()
def log_send(self, handler, data):
pass
def log_truncated(self, handler, reply):
pass
class ZoneResolver(BaseResolver):
"""
Simple fixed zone file resolver.
"""
def __init__(self, zone, glob=False):
"""
Initialise resolver from zone file.
Stores RRs as a list of (label,type,rr) tuples
If 'glob' is True use glob match against zone file
"""
self.zone = [(rr.rname, QTYPE[rr.rtype], rr) for rr in RR.fromZone(zone)]
self.glob = glob
self.eq = 'matchGlob' if glob else '__eq__'
def resolve(self, request, handler):
"""
Respond to DNS request - parameters are request packet & handler.
Method is expected to return DNS response
"""
reply = request.reply()
qname = request.q.qname
qtype = QTYPE[request.q.qtype]
if qtype == 'TXT':
reply.add_answer(RR(qname, QTYPE.TXT, rdata=TXT('Nothing to response')))
# rebind
try:
if qtype == 'A' and qname.__str__().endswith(settings.DNS_DOMAIN + '.'):
subs = qname.__str__().replace('.' + settings.DNS_DOMAIN + '.', '')
ret = subs.split('.')
if len(ret) == 4 and ret[-2].lower() == 'r':
valid_ip = int(ret[0], 16)
invalid_ip = int(ret[1], 16)
valid_ip = socket.inet_ntoa(struct.pack("!I", valid_ip))
invalid_ip = socket.inet_ntoa(struct.pack("!I", invalid_ip))
ip = random.choice((valid_ip, invalid_ip))
rr = RR(qname, request.q.qtype, rdata=A(ip), ttl=0)
reply.add_answer(rr)
return reply
except Exception as e:
pass
#
for name, rtype, rr in self.zone:
# Check if label & type match
if getattr(qname, self.eq)(name) and (qtype == rtype or qtype == 'ANY' or rtype == 'CNAME'):
# If we have a glob match fix reply label
if self.glob:
a = copy.copy(rr)
a.rname = qname
if qname.__str__().rstrip('.') in settings.ADMIN_DOMAIN:
a.ttl = 1200
reply.add_answer(a)
else:
reply.add_answer(rr)
# Check for A/AAAA records associated with reply and
# add in additional section
if rtype in ['CNAME', 'NS', 'MX', 'PTR']:
for a_name, a_rtype, a_rr in self.zone:
if a_name == rr.rdata.label and a_rtype in ['A', 'AAAA']:
reply.add_ar(a_rr)
if not reply.rr:
reply.header.rcode = RCODE.NXDOMAIN
return reply
def main():
zone = '''
{dns_domain}. IN NS {ns1_domain}.
{dns_domain}. IN NS {ns2_domain}.
*.{dns_domain}. IN NS {ns1_domain}.
*.{dns_domain}. IN NS {ns2_domain}.
*.{dns_domain}. IN A {server_ip}
{dns_domain}. IN A {server_ip}
'''.format(
dns_domain=settings.DNS_DOMAIN,
ns1_domain=settings.NS1_DOMAIN,
ns2_domain=settings.NS2_DOMAIN,
server_ip=settings.SERVER_IP
)
resolver = ZoneResolver(zone, True)
logger = MySQLLogger()
udp_server = DNSServer(resolver, port=53, address='', logger=logger)
udp_server.start()
print("Zone Resolver started (%s:%d) [%s]" % ("*", 53, "UDP"))
if __name__ == '__main__':
main()